pymenuconfig.py 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. # SPDX-License-Identifier: ISC
  2. # -*- coding: utf-8 -*-
  3. """
  4. Overview
  5. ========
  6. pymenuconfig is a small and simple frontend to Kconfiglib that's written
  7. entirely in Python using Tkinter as its GUI toolkit.
  8. Motivation
  9. ==========
  10. Kconfig is a nice and powerful framework for build-time configuration and lots
  11. of projects already benefit from using it. Kconfiglib allows to utilize power of
  12. Kconfig by using scripts written in pure Python, without requiring one to build
  13. Linux kernel tools written in C (this can be quite tedious on anything that's
  14. not *nix). The aim of this project is to implement simple and small Kconfiglib
  15. GUI frontend that runs on as much systems as possible.
  16. Tkinter GUI toolkit is a natural choice if portability is considered, as it's
  17. a part of Python standard library and is available virtually in every CPython
  18. installation.
  19. User interface
  20. ==============
  21. I've tried to replicate look and fill of Linux kernel 'menuconfig' tool that
  22. many users are used to, including keyboard-oriented control and textual
  23. representation of menus with fixed-width font.
  24. Usage
  25. =====
  26. The pymenuconfig module is executable and parses command-line args, so the
  27. most simple way to run menuconfig is to execute script directly:
  28. python pymenuconfig.py --kconfig Kconfig
  29. As with most command-line tools list of options can be obtained with '--help':
  30. python pymenuconfig.py --help
  31. If installed with setuptools, one can run it like this:
  32. python -m pymenuconfig --kconfig Kconfig
  33. In case you're making a wrapper around menuconfig, you can either call main():
  34. import pymenuconfig
  35. pymenuconfig.main(['--kconfig', 'Kconfig'])
  36. Or import MenuConfig class, instantiate it and manually run Tkinter's mainloop:
  37. import tkinter
  38. import kconfiglib
  39. from pymenuconfig import MenuConfig
  40. kconfig = kconfiglib.Kconfig()
  41. mconf = MenuConfig(kconfig)
  42. tkinter.mainloop()
  43. """
  44. from __future__ import print_function
  45. import os
  46. import sys
  47. import argparse
  48. import kconfiglib
  49. # Tk is imported differently depending on python major version
  50. if sys.version_info[0] < 3:
  51. import Tkinter as tk
  52. import tkFont as font
  53. import tkFileDialog as filedialog
  54. import tkMessageBox as messagebox
  55. else:
  56. import tkinter as tk
  57. from tkinter import font
  58. from tkinter import filedialog
  59. from tkinter import messagebox
  60. class ListEntry(object):
  61. """
  62. Represents visible menu node and holds all information related to displaying
  63. menu node in a Listbox.
  64. Instances of this class also handle all interaction with main window.
  65. A node is displayed as a single line of text:
  66. PREFIX INDENT BODY POSTFIX
  67. - The PREFIX is always 3 characters or more and can take following values:
  68. ' ' comment, menu, bool choice, etc.
  69. Inside menus:
  70. '< >' bool symbol has value 'n'
  71. '<*>' bool symbol has value 'y'
  72. '[ ]' tristate symbol has value 'n'
  73. '[M]' tristate symbol has value 'm'
  74. '[*]' tristate symbol has value 'y'
  75. '- -' symbol has value 'n' that's not editable
  76. '-M-' symbol has value 'm' that's not editable
  77. '-*-' symbol has value 'y' that's not editable
  78. '(M)' tristate choice has value 'm'
  79. '(*)' tristate choice has value 'y'
  80. '(some value)' value of non-bool/tristate symbols
  81. Inside choices:
  82. '( )' symbol has value 'n'
  83. '(M)' symbol has value 'm'
  84. '(*)' symbol has value 'y'
  85. - INDENT is a sequence of space characters. It's used in implicit menus, and
  86. adds 2 spaces for each nesting level
  87. - BODY is a menu node prompt. '***' is added if node is a comment
  88. - POSTFIX adds '(NEW)', '--->' and selected choice symbol where applicable
  89. Attributes:
  90. node:
  91. MenuNode instance this ListEntry is created for.
  92. visible:
  93. Whether entry should be shown in main window.
  94. text:
  95. String to display in a main window's Listbox.
  96. refresh():
  97. Updates .visible and .text attribute values.
  98. set_tristate_value():
  99. Set value for bool/tristate symbols, value should be one of 0,1,2 or None.
  100. Usually it's called when user presses 'y', 'n', 'm' key.
  101. set_str_value():
  102. Set value for non-bool/tristate symbols, value is a string. Usually called
  103. with a value returned by one of MenuConfig.ask_for_* methods.
  104. toggle():
  105. Toggle bool/tristate symbol value. Called when '<Space>' key is pressed in
  106. a main window. Also selects choice value.
  107. select():
  108. Called when '<Return>' key is pressed in a main window with 'SELECT'
  109. action selected. Displays submenu, choice selection menu, or just selects
  110. choice value. For non-bool/tristate symbols asks MenuConfig window to
  111. handle value input via one of MenuConfig.ask_for_* methods.
  112. show_help():
  113. Called when '<Return>' key is pressed in a main window with 'HELP' action
  114. selected. Prepares text help and calls MenuConfig.show_text() to display
  115. text window.
  116. """
  117. # How to display value of BOOL and TRISTATE symbols
  118. TRI_TO_DISPLAY = {
  119. 0: ' ',
  120. 1: 'M',
  121. 2: '*'
  122. }
  123. def __init__(self, mconf, node, indent):
  124. self.indent = indent
  125. self.node = node
  126. self.menuconfig = mconf
  127. self.visible = False
  128. self.text = None
  129. def __str__(self):
  130. return self.text
  131. def _is_visible(self):
  132. node = self.node
  133. v = True
  134. v = v and node.prompt is not None
  135. # It should be enough to check if prompt expression is not false and
  136. # for menu nodes whether 'visible if' is not false
  137. v = v and kconfiglib.expr_value(node.prompt[1]) > 0
  138. if node.item == kconfiglib.MENU:
  139. v = v and kconfiglib.expr_value(node.visibility) > 0
  140. # If node references Symbol, then we also account for symbol visibility
  141. # TODO: need to re-think whether this is needed
  142. if isinstance(node.item, kconfiglib.Symbol):
  143. if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  144. v = v and len(node.item.assignable) > 0
  145. else:
  146. v = v and node.item.visibility > 0
  147. return v
  148. def _get_text(self):
  149. """
  150. Compute textual representation of menu node (a line in ListView)
  151. """
  152. node = self.node
  153. item = node.item
  154. # Determine prefix
  155. prefix = ' '
  156. if (isinstance(item, kconfiglib.Symbol) and item.choice is None or
  157. isinstance(item, kconfiglib.Choice) and item.type is kconfiglib.TRISTATE):
  158. # The node is for either a symbol outside of choice statement
  159. # or a tristate choice
  160. if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  161. value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
  162. if len(item.assignable) > 1:
  163. # Symbol is editable
  164. if 1 in item.assignable:
  165. prefix = '<{}>'.format(value)
  166. else:
  167. prefix = '[{}]'.format(value)
  168. else:
  169. # Symbol is not editable
  170. prefix = '-{}-'.format(value)
  171. else:
  172. prefix = '({})'.format(item.str_value)
  173. elif isinstance(item, kconfiglib.Symbol) and item.choice is not None:
  174. # The node is for symbol inside choice statement
  175. if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  176. value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
  177. if len(item.assignable) > 0:
  178. # Symbol is editable
  179. prefix = '({})'.format(value)
  180. else:
  181. # Symbol is not editable
  182. prefix = '-{}-'.format(value)
  183. else:
  184. prefix = '({})'.format(item.str_value)
  185. # Prefix should be at least 3 chars long
  186. if len(prefix) < 3:
  187. prefix += ' ' * (3 - len(prefix))
  188. # Body
  189. body = ''
  190. if node.prompt is not None:
  191. if item is kconfiglib.COMMENT:
  192. body = '*** {} ***'.format(node.prompt[0])
  193. else:
  194. body = node.prompt[0]
  195. # Suffix
  196. is_menu = False
  197. is_new = False
  198. if (item is kconfiglib.MENU
  199. or isinstance(item, kconfiglib.Symbol) and node.is_menuconfig
  200. or isinstance(item, kconfiglib.Choice)):
  201. is_menu = True
  202. if isinstance(item, kconfiglib.Symbol) and item.user_value is None:
  203. is_new = True
  204. # For symbol inside choice that has 'y' value, '(NEW)' is not displayed
  205. if (isinstance(item, kconfiglib.Symbol)
  206. and item.choice and item.choice.tri_value == 2):
  207. is_new = False
  208. # Choice selection - displayed only for choices which have 'y' value
  209. choice_selection = None
  210. if isinstance(item, kconfiglib.Choice) and node.item.str_value == 'y':
  211. choice_selection = ''
  212. if item.selection is not None:
  213. sym = item.selection
  214. if sym.nodes and sym.nodes[0].prompt is not None:
  215. choice_selection = sym.nodes[0].prompt[0]
  216. text = ' {prefix} {indent}{body}{choice}{new}{menu}'.format(
  217. prefix=prefix,
  218. indent=' ' * self.indent,
  219. body=body,
  220. choice='' if choice_selection is None else ' ({})'.format(
  221. choice_selection
  222. ),
  223. new=' (NEW)' if is_new else '',
  224. menu=' --->' if is_menu else ''
  225. )
  226. return text
  227. def refresh(self):
  228. self.visible = self._is_visible()
  229. self.text = self._get_text()
  230. def set_tristate_value(self, value):
  231. """
  232. Call to change value of BOOL, TRISTATE symbols
  233. It's preferred to use this instead of item.set_value as it handles
  234. all necessary interaction with MenuConfig window when symbol value
  235. changes
  236. None value is accepted but ignored
  237. """
  238. item = self.node.item
  239. if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
  240. and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)
  241. and value is not None):
  242. if value in item.assignable:
  243. item.set_value(value)
  244. elif value == 2 and 1 in item.assignable:
  245. print(
  246. 'Symbol {} value is limited to \'m\'. Setting value \'m\' instead of \'y\''.format(item.name),
  247. file=sys.stderr
  248. )
  249. item.set_value(1)
  250. self.menuconfig.mark_as_changed()
  251. self.menuconfig.refresh_display()
  252. def set_str_value(self, value):
  253. """
  254. Call to change value of HEX, INT, STRING symbols
  255. It's preferred to use this instead of item.set_value as it handles
  256. all necessary interaction with MenuConfig window when symbol value
  257. changes
  258. None value is accepted but ignored
  259. """
  260. item = self.node.item
  261. if (isinstance(item, kconfiglib.Symbol)
  262. and item.type in (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)
  263. and value is not None):
  264. item.set_value(value)
  265. self.menuconfig.mark_as_changed()
  266. self.menuconfig.refresh_display()
  267. def toggle(self):
  268. """
  269. Called when <space> key is pressed
  270. """
  271. item = self.node.item
  272. if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
  273. and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)):
  274. value = item.tri_value
  275. # Find next value in Symbol/Choice.assignable, or use assignable[0]
  276. try:
  277. it = iter(item.assignable)
  278. while value != next(it):
  279. pass
  280. self.set_tristate_value(next(it))
  281. except StopIteration:
  282. self.set_tristate_value(item.assignable[0])
  283. def select(self):
  284. """
  285. Called when <Return> key is pressed and SELECT action is selected
  286. """
  287. item = self.node.item
  288. # - Menu: dive into submenu
  289. # - INT, HEX, STRING symbol: raise prompt to enter symbol value
  290. # - BOOL, TRISTATE symbol inside 'y'-valued Choice: set 'y' value
  291. if (item is kconfiglib.MENU
  292. or isinstance(item, kconfiglib.Symbol) and self.node.is_menuconfig
  293. or isinstance(item, kconfiglib.Choice)):
  294. # Dive into submenu
  295. self.menuconfig.show_submenu(self.node)
  296. elif (isinstance(item, kconfiglib.Symbol) and item.type in
  297. (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)):
  298. # Raise prompt to enter symbol value
  299. ident = self.node.prompt[0] if self.node.prompt is not None else None
  300. title = 'Symbol: {}'.format(item.name)
  301. if item.type is kconfiglib.INT:
  302. # Find enabled ranges
  303. ranges = [
  304. (int(start.str_value), int(end.str_value))
  305. for start, end, expr in item.ranges
  306. if kconfiglib.expr_value(expr) > 0
  307. ]
  308. # Raise prompt
  309. self.set_str_value(str(self.menuconfig.ask_for_int(
  310. ident=ident,
  311. title=title,
  312. value=item.str_value,
  313. ranges=ranges
  314. )))
  315. elif item.type is kconfiglib.HEX:
  316. # Find enabled ranges
  317. ranges = [
  318. (int(start.str_value, base=16), int(end.str_value, base=16))
  319. for start, end, expr in item.ranges
  320. if kconfiglib.expr_value(expr) > 0
  321. ]
  322. # Raise prompt
  323. self.set_str_value(hex(self.menuconfig.ask_for_hex(
  324. ident=ident,
  325. title=title,
  326. value=item.str_value,
  327. ranges=ranges
  328. )))
  329. elif item.type is kconfiglib.STRING:
  330. # Raise prompt
  331. self.set_str_value(self.menuconfig.ask_for_string(
  332. ident=ident,
  333. title=title,
  334. value=item.str_value
  335. ))
  336. elif (isinstance(item, kconfiglib.Symbol)
  337. and item.choice is not None and item.choice.tri_value == 2):
  338. # Symbol inside choice -> set symbol value to 'y'
  339. self.set_tristate_value(2)
  340. def show_help(self):
  341. node = self.node
  342. item = self.node.item
  343. if isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)):
  344. title = 'Help for symbol: {}'.format(item.name)
  345. if node.help:
  346. help = node.help
  347. else:
  348. help = 'There is no help available for this option.\n'
  349. lines = []
  350. lines.append(help)
  351. lines.append(
  352. 'Symbol: {} [={}]'.format(
  353. item.name if item.name else '<UNNAMED>', item.str_value
  354. )
  355. )
  356. lines.append('Type : {}'.format(kconfiglib.TYPE_TO_STR[item.type]))
  357. for n in item.nodes:
  358. lines.append('Prompt: {}'.format(n.prompt[0] if n.prompt else '<EMPTY>'))
  359. lines.append(' Defined at {}:{}'.format(n.filename, n.linenr))
  360. lines.append(' Depends on: {}'.format(kconfiglib.expr_str(n.dep)))
  361. text = '\n'.join(lines)
  362. else:
  363. title = 'Help'
  364. text = 'Help not available for this menu node.\n'
  365. self.menuconfig.show_text(text, title)
  366. self.menuconfig.refresh_display()
  367. class EntryDialog(object):
  368. """
  369. Creates modal dialog (top-level Tk window) with labels, entry box and two
  370. buttons: OK and CANCEL.
  371. """
  372. def __init__(self, master, text, title, ident=None, value=None):
  373. self.master = master
  374. dlg = self.dlg = tk.Toplevel(master)
  375. dlg.title(title)
  376. # Identifier label
  377. if ident is not None:
  378. self.label_id = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
  379. self.label_id['font'] = font.nametofont('TkFixedFont')
  380. self.label_id['text'] = '# {}'.format(ident)
  381. self.label_id.pack(fill=tk.X, padx=2, pady=2)
  382. # Label
  383. self.label = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
  384. self.label['font'] = font.nametofont('TkFixedFont')
  385. self.label['text'] = text
  386. self.label.pack(fill=tk.X, padx=10, pady=4)
  387. # Entry box
  388. self.entry = tk.Entry(dlg)
  389. self.entry['font'] = font.nametofont('TkFixedFont')
  390. self.entry.pack(fill=tk.X, padx=2, pady=2)
  391. # Frame for buttons
  392. self.frame = tk.Frame(dlg)
  393. self.frame.pack(padx=2, pady=2)
  394. # Button
  395. self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
  396. self.btn_accept['font'] = font.nametofont('TkFixedFont')
  397. self.btn_accept.pack(side=tk.LEFT, padx=2)
  398. self.btn_cancel = tk.Button(self.frame, text='< Cancel >', command=self.cancel)
  399. self.btn_cancel['font'] = font.nametofont('TkFixedFont')
  400. self.btn_cancel.pack(side=tk.LEFT, padx=2)
  401. # Bind Enter and Esc keys
  402. self.dlg.bind('<Return>', self.accept)
  403. self.dlg.bind('<Escape>', self.cancel)
  404. # Dialog is resizable only by width
  405. self.dlg.resizable(1, 0)
  406. # Set supplied value (if any)
  407. if value is not None:
  408. self.entry.insert(0, value)
  409. self.entry.selection_range(0, tk.END)
  410. # By default returned value is None. To caller this means that entry
  411. # process was cancelled
  412. self.value = None
  413. # Modal dialog
  414. dlg.transient(master)
  415. dlg.grab_set()
  416. # Center dialog window
  417. _center_window_above_parent(master, dlg)
  418. # Focus entry field
  419. self.entry.focus_set()
  420. def accept(self, ev=None):
  421. self.value = self.entry.get()
  422. self.dlg.destroy()
  423. def cancel(self, ev=None):
  424. self.dlg.destroy()
  425. class TextDialog(object):
  426. def __init__(self, master, text, title):
  427. self.master = master
  428. dlg = self.dlg = tk.Toplevel(master)
  429. dlg.title(title)
  430. dlg.minsize(600,400)
  431. # Text
  432. self.text = tk.Text(dlg, height=1)
  433. self.text['font'] = font.nametofont('TkFixedFont')
  434. self.text.insert(tk.END, text)
  435. # Make text read-only
  436. self.text['state'] = tk.DISABLED
  437. self.text.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
  438. # Frame for buttons
  439. self.frame = tk.Frame(dlg)
  440. self.frame.pack(padx=2, pady=2)
  441. # Button
  442. self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
  443. self.btn_accept['font'] = font.nametofont('TkFixedFont')
  444. self.btn_accept.pack(side=tk.LEFT, padx=2)
  445. # Bind Enter and Esc keys
  446. self.dlg.bind('<Return>', self.accept)
  447. self.dlg.bind('<Escape>', self.cancel)
  448. # Modal dialog
  449. dlg.transient(master)
  450. dlg.grab_set()
  451. # Center dialog window
  452. _center_window_above_parent(master, dlg)
  453. # Focus entry field
  454. self.text.focus_set()
  455. def accept(self, ev=None):
  456. self.dlg.destroy()
  457. def cancel(self, ev=None):
  458. self.dlg.destroy()
  459. class MenuConfig(object):
  460. (
  461. ACTION_SELECT,
  462. ACTION_EXIT,
  463. ACTION_HELP,
  464. ACTION_LOAD,
  465. ACTION_SAVE,
  466. ACTION_SAVE_AS
  467. ) = range(6)
  468. ACTIONS = (
  469. ('Select', ACTION_SELECT),
  470. ('Exit', ACTION_EXIT),
  471. ('Help', ACTION_HELP),
  472. ('Load', ACTION_LOAD),
  473. ('Save', ACTION_SAVE),
  474. ('Save as', ACTION_SAVE_AS),
  475. )
  476. def __init__(self, kconfig):
  477. self.kconfig = kconfig
  478. # Instantiate Tk widgets
  479. self.root = tk.Tk()
  480. dlg = self.root
  481. # Window title
  482. dlg.title('pymenuconfig')
  483. # Some empirical window size
  484. dlg.minsize(500, 300)
  485. dlg.geometry('800x600')
  486. # Label that shows position in menu tree
  487. self.label_position = tk.Label(
  488. dlg,
  489. anchor=tk.W,
  490. justify=tk.LEFT,
  491. font=font.nametofont('TkFixedFont')
  492. )
  493. self.label_position.pack(fill=tk.X, padx=2)
  494. # 'Tip' frame and text
  495. self.frame_tip = tk.LabelFrame(
  496. dlg,
  497. text='Tip'
  498. )
  499. self.label_tip = tk.Label(
  500. self.frame_tip,
  501. anchor=tk.W,
  502. justify=tk.LEFT,
  503. font=font.nametofont('TkFixedFont')
  504. )
  505. self.label_tip['text'] = '\n'.join([
  506. 'Arrow keys navigate the menu. <Enter> performs selected operation (set of buttons at the bottom)',
  507. 'Pressing <Y> includes, <N> excludes, <M> modularizes features',
  508. 'Press <Esc> to go one level up. Press <Esc> at top level to exit',
  509. 'Legend: [*] built-in [ ] excluded <M> module < > module capable'
  510. ])
  511. self.label_tip.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
  512. self.frame_tip.pack(fill=tk.X, padx=2)
  513. # Main ListBox where all the magic happens
  514. self.list = tk.Listbox(
  515. dlg,
  516. selectmode=tk.SINGLE,
  517. activestyle=tk.UNDERLINE,
  518. font=font.nametofont('TkFixedFont'),
  519. height=1,
  520. )
  521. # Make selection invisible
  522. self.list['selectbackground'] = self.list['background']
  523. self.list['selectforeground'] = self.list['foreground']
  524. self.list.pack(fill=tk.BOTH, expand=1, padx=20, ipadx=2)
  525. # Frame with radio buttons
  526. self.frame_radio = tk.Frame(dlg)
  527. self.radio_buttons = []
  528. self.tk_selected_action = tk.IntVar()
  529. for text, value in MenuConfig.ACTIONS:
  530. btn = tk.Radiobutton(
  531. self.frame_radio,
  532. variable=self.tk_selected_action,
  533. value=value
  534. )
  535. btn['text'] = '< {} >'.format(text)
  536. btn['font'] = font.nametofont('TkFixedFont')
  537. btn['indicatoron'] = 0
  538. btn.pack(side=tk.LEFT)
  539. self.radio_buttons.append(btn)
  540. self.frame_radio.pack(anchor=tk.CENTER, pady=4)
  541. # Label with status information
  542. self.tk_status = tk.StringVar()
  543. self.label_status = tk.Label(
  544. dlg,
  545. textvariable=self.tk_status,
  546. anchor=tk.W,
  547. justify=tk.LEFT,
  548. font=font.nametofont('TkFixedFont')
  549. )
  550. self.label_status.pack(fill=tk.X, padx=4, pady=4)
  551. # Center window
  552. _center_window(self.root, dlg)
  553. # Disable keyboard focus on all widgets ...
  554. self._set_option_to_all_children(dlg, 'takefocus', 0)
  555. # ... except for main ListBox
  556. self.list['takefocus'] = 1
  557. self.list.focus_set()
  558. # Bind keys
  559. dlg.bind('<Escape>', self.handle_keypress)
  560. dlg.bind('<space>', self.handle_keypress)
  561. dlg.bind('<Return>', self.handle_keypress)
  562. dlg.bind('<Right>', self.handle_keypress)
  563. dlg.bind('<Left>', self.handle_keypress)
  564. dlg.bind('n', self.handle_keypress)
  565. dlg.bind('m', self.handle_keypress)
  566. dlg.bind('y', self.handle_keypress)
  567. # Register callback that's called when window closes
  568. dlg.wm_protocol('WM_DELETE_WINDOW', self._close_window)
  569. # Init fields
  570. self.node = None
  571. self.node_stack = []
  572. self.all_entries = []
  573. self.shown_entries = []
  574. self.config_path = None
  575. self.unsaved_changes = False
  576. self.status_string = 'NEW CONFIG'
  577. self.update_status()
  578. # Display first child of top level node (the top level node is 'mainmenu')
  579. self.show_node(self.kconfig.top_node)
  580. def _set_option_to_all_children(self, widget, option, value):
  581. widget[option] = value
  582. for n,c in widget.children.items():
  583. self._set_option_to_all_children(c, option, value)
  584. @property
  585. def _selected_entry(self):
  586. # type: (...) -> ListEntry
  587. active_idx = self.list.index(tk.ACTIVE)
  588. if active_idx >= 0 and active_idx < len(self.shown_entries):
  589. return self.shown_entries[active_idx]
  590. return None
  591. def _select_node(self, node):
  592. # type: (kconfiglib.MenuNode) -> None
  593. """
  594. Attempts to select entry that corresponds to given MenuNode in main listbox
  595. """
  596. idx = None
  597. for i, e in enumerate(self.shown_entries):
  598. if e.node is node:
  599. idx = i
  600. break
  601. if idx is not None:
  602. self.list.activate(idx)
  603. self.list.see(idx)
  604. def handle_keypress(self, ev):
  605. keysym = ev.keysym
  606. if keysym == 'Left':
  607. self._select_action(prev=True)
  608. elif keysym == 'Right':
  609. self._select_action(prev=False)
  610. elif keysym == 'space':
  611. self._selected_entry.toggle()
  612. elif keysym in ('n', 'm', 'y'):
  613. self._selected_entry.set_tristate_value(kconfiglib.STR_TO_TRI[keysym])
  614. elif keysym == 'Return':
  615. action = self.tk_selected_action.get()
  616. if action == self.ACTION_SELECT:
  617. self._selected_entry.select()
  618. elif action == self.ACTION_EXIT:
  619. self._action_exit()
  620. elif action == self.ACTION_HELP:
  621. self._selected_entry.show_help()
  622. elif action == self.ACTION_LOAD:
  623. if self.prevent_losing_changes():
  624. self.open_config()
  625. elif action == self.ACTION_SAVE:
  626. self.save_config()
  627. elif action == self.ACTION_SAVE_AS:
  628. self.save_config(force_file_dialog=True)
  629. elif keysym == 'Escape':
  630. self._action_exit()
  631. pass
  632. def _close_window(self):
  633. if self.prevent_losing_changes():
  634. print('Exiting..')
  635. self.root.destroy()
  636. def _action_exit(self):
  637. if self.node_stack:
  638. self.show_parent()
  639. else:
  640. self._close_window()
  641. def _select_action(self, prev=False):
  642. # Determine the radio button that's activated
  643. action = self.tk_selected_action.get()
  644. if prev:
  645. action -= 1
  646. else:
  647. action += 1
  648. action %= len(MenuConfig.ACTIONS)
  649. self.tk_selected_action.set(action)
  650. def _collect_list_entries(self, start_node, indent=0):
  651. """
  652. Given first MenuNode of nodes list at some level in menu hierarchy,
  653. collects nodes that may be displayed when viewing and editing that
  654. hierarchy level. Includes implicit menu nodes, i.e. the ones dependent
  655. on 'config' entry via 'if' statement which are internally represented
  656. as children of their dependency
  657. """
  658. entries = []
  659. n = start_node
  660. while n is not None:
  661. entries.append(ListEntry(self, n, indent))
  662. # If node refers to a symbol (X) and has children, it is either
  663. # 'config' or 'menuconfig'. The children are items inside 'if X'
  664. # block that immediately follows 'config' or 'menuconfig' entry.
  665. # If it's a 'menuconfig' then corresponding MenuNode is shown as a
  666. # regular menu entry. But if it's a 'config', then its children need
  667. # to be shown in the same list with their texts indented
  668. if (n.list is not None
  669. and isinstance(n.item, kconfiglib.Symbol)
  670. and n.is_menuconfig == False):
  671. entries.extend(
  672. self._collect_list_entries(n.list, indent=indent + 1)
  673. )
  674. n = n.next
  675. return entries
  676. def refresh_display(self, reset_selection=False):
  677. # Refresh list entries' attributes
  678. for e in self.all_entries:
  679. e.refresh()
  680. # Try to preserve selection upon refresh
  681. selected_entry = self._selected_entry
  682. # Also try to preserve listbox scroll offset
  683. # If not preserved, the see() method will make wanted item to appear
  684. # at the bottom of the list, even if previously it was in center
  685. scroll_offset = self.list.yview()[0]
  686. # Show only visible entries
  687. self.shown_entries = [e for e in self.all_entries if e.visible]
  688. # Refresh listbox contents
  689. self.list.delete(0, tk.END)
  690. self.list.insert(0, *self.shown_entries)
  691. if selected_entry and not reset_selection:
  692. # Restore scroll position
  693. self.list.yview_moveto(scroll_offset)
  694. # Activate previously selected node
  695. self._select_node(selected_entry.node)
  696. else:
  697. # Select the topmost entry
  698. self.list.activate(0)
  699. # Select ACTION_SELECT on each refresh (mimic C menuconfig)
  700. self.tk_selected_action.set(self.ACTION_SELECT)
  701. # Display current location in configuration tree
  702. pos = []
  703. for n in self.node_stack + [self.node]:
  704. pos.append(n.prompt[0] if n.prompt else '[none]')
  705. self.label_position['text'] = u'# ' + u' → '.join(pos)
  706. def show_node(self, node):
  707. self.node = node
  708. if node.list is not None:
  709. self.all_entries = self._collect_list_entries(node.list)
  710. else:
  711. self.all_entries = []
  712. self.refresh_display(reset_selection=True)
  713. def show_submenu(self, node):
  714. self.node_stack.append(self.node)
  715. self.show_node(node)
  716. def show_parent(self):
  717. if self.node_stack:
  718. select_node = self.node
  719. parent_node = self.node_stack.pop()
  720. self.show_node(parent_node)
  721. # Restore previous selection
  722. self._select_node(select_node)
  723. def ask_for_string(self, ident=None, title='Enter string', value=None):
  724. """
  725. Raises dialog with text entry widget and asks user to enter string
  726. Return:
  727. - str - user entered string
  728. - None - entry was cancelled
  729. """
  730. text = 'Please enter a string value\n' \
  731. 'User <Enter> key to accept the value\n' \
  732. 'Use <Esc> key to cancel entry\n'
  733. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  734. self.root.wait_window(d.dlg)
  735. self.list.focus_set()
  736. return d.value
  737. def ask_for_int(self, ident=None, title='Enter integer value', value=None, ranges=()):
  738. """
  739. Raises dialog with text entry widget and asks user to enter decimal number
  740. Ranges should be iterable of tuples (start, end),
  741. where 'start' and 'end' specify allowed value range (inclusively)
  742. Return:
  743. - int - when valid number that falls within any one of specified ranges is entered
  744. - None - invalid number or entry was cancelled
  745. """
  746. text = 'Please enter a decimal value. Fractions will not be accepted\n' \
  747. 'User <Enter> key to accept the value\n' \
  748. 'Use <Esc> key to cancel entry\n'
  749. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  750. self.root.wait_window(d.dlg)
  751. self.list.focus_set()
  752. ivalue = None
  753. if d.value:
  754. try:
  755. ivalue = int(d.value)
  756. except ValueError:
  757. messagebox.showerror('Bad value', 'Entered value \'{}\' is not an integer'.format(d.value))
  758. if ivalue is not None and ranges:
  759. allowed = False
  760. for start, end in ranges:
  761. allowed = allowed or start <= ivalue and ivalue <= end
  762. if not allowed:
  763. messagebox.showerror(
  764. 'Bad value',
  765. 'Entered value \'{:d}\' is out of range\n'
  766. 'Allowed:\n{}'.format(
  767. ivalue,
  768. '\n'.join([' {:d} - {:d}'.format(s,e) for s,e in ranges])
  769. )
  770. )
  771. ivalue = None
  772. return ivalue
  773. def ask_for_hex(self, ident=None, title='Enter hexadecimal value', value=None, ranges=()):
  774. """
  775. Raises dialog with text entry widget and asks user to enter decimal number
  776. Ranges should be iterable of tuples (start, end),
  777. where 'start' and 'end' specify allowed value range (inclusively)
  778. Return:
  779. - int - when valid number that falls within any one of specified ranges is entered
  780. - None - invalid number or entry was cancelled
  781. """
  782. text = 'Please enter a hexadecimal value\n' \
  783. 'User <Enter> key to accept the value\n' \
  784. 'Use <Esc> key to cancel entry\n'
  785. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  786. self.root.wait_window(d.dlg)
  787. self.list.focus_set()
  788. hvalue = None
  789. if d.value:
  790. try:
  791. hvalue = int(d.value, base=16)
  792. except ValueError:
  793. messagebox.showerror('Bad value', 'Entered value \'{}\' is not a hexadecimal value'.format(d.value))
  794. if hvalue is not None and ranges:
  795. allowed = False
  796. for start, end in ranges:
  797. allowed = allowed or start <= hvalue and hvalue <= end
  798. if not allowed:
  799. messagebox.showerror(
  800. 'Bad value',
  801. 'Entered value \'0x{:x}\' is out of range\n'
  802. 'Allowed:\n{}'.format(
  803. hvalue,
  804. '\n'.join([' 0x{:x} - 0x{:x}'.format(s,e) for s,e in ranges])
  805. )
  806. )
  807. hvalue = None
  808. return hvalue
  809. def show_text(self, text, title='Info'):
  810. """
  811. Raises dialog with read-only text view that contains supplied text
  812. """
  813. d = TextDialog(self.root, text, title)
  814. self.root.wait_window(d.dlg)
  815. self.list.focus_set()
  816. def mark_as_changed(self):
  817. """
  818. Marks current config as having unsaved changes
  819. Should be called whenever config value is changed
  820. """
  821. self.unsaved_changes = True
  822. self.update_status()
  823. def set_status_string(self, status):
  824. """
  825. Sets status string displayed at the bottom of the window
  826. """
  827. self.status_string = status
  828. self.update_status()
  829. def update_status(self):
  830. """
  831. Updates status bar display
  832. Status bar displays:
  833. - unsaved status
  834. - current config path
  835. - status string (see set_status_string())
  836. """
  837. self.tk_status.set('{} [{}] {}'.format(
  838. '<UNSAVED>' if self.unsaved_changes else '',
  839. self.config_path if self.config_path else '',
  840. self.status_string
  841. ))
  842. def prevent_losing_changes(self):
  843. """
  844. Checks if there are unsaved changes and asks user to save or discard them
  845. This routine should be called whenever current config is going to be discarded
  846. Raises the usual 'Yes', 'No', 'Cancel' prompt.
  847. Return:
  848. - True: caller may safely drop current config state
  849. - False: user needs to continue work on current config ('Cancel' pressed or saving failed)
  850. """
  851. if not self.unsaved_changes:
  852. return True
  853. res = messagebox.askyesnocancel(
  854. parent=self.root,
  855. title='Unsaved changes',
  856. message='Config has unsaved changes. Do you want to save them?'
  857. )
  858. if res is None:
  859. return False
  860. elif res is False:
  861. return True
  862. # Otherwise attempt to save config and succeed only if config has been saved successfully
  863. saved = self.save_config()
  864. return saved
  865. def open_config(self, path=None):
  866. if path is None:
  867. # Create open dialog. Either existing file is selected or no file is selected as a result
  868. path = filedialog.askopenfilename(
  869. parent=self.root,
  870. title='Open config..',
  871. initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
  872. filetypes=(('.config files', '*.config'), ('All files', '*.*'))
  873. )
  874. if not path or not os.path.isfile(path):
  875. return False
  876. path = os.path.abspath(path)
  877. print('Loading config: \'{}\''.format(path))
  878. # Try to open given path
  879. # If path does not exist, we still set current config path to it but don't load anything
  880. self.unsaved_changes = False
  881. self.config_path = path
  882. if not os.path.exists(path):
  883. self.set_status_string('New config')
  884. self.mark_as_changed()
  885. return True
  886. # Load config and set status accordingly
  887. try:
  888. self.kconfig.load_config(path)
  889. except IOError as e:
  890. self.set_status_string('Failed to load: \'{}\''.format(path))
  891. self.refresh_display()
  892. print('Failed to load config \'{}\': {}'.format(path, e))
  893. return False
  894. self.set_status_string('Opened config')
  895. self.refresh_display()
  896. return True
  897. def save_config(self, force_file_dialog=False):
  898. path = self.config_path
  899. if path is None or force_file_dialog:
  900. path = filedialog.asksaveasfilename(
  901. parent=self.root,
  902. title='Save config as..',
  903. initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
  904. initialfile=os.path.basename(self.config_path) if self.config_path else None,
  905. defaultextension='.config',
  906. filetypes=(('.config files', '*.config'), ('All files', '*.*'))
  907. )
  908. if not path:
  909. return False
  910. path = os.path.abspath(path)
  911. print('Saving config: \'{}\''.format(path))
  912. # Try to save config to selected path
  913. try:
  914. self.kconfig.write_config(path)
  915. self.unsaved_changes = False
  916. self.config_path = path
  917. self.set_status_string('Saved config')
  918. except IOError as e:
  919. self.set_status_string('Failed to save: \'{}\''.format(path))
  920. print('Save failed: {}'.format(e), file=sys.stderr)
  921. return False
  922. return True
  923. def _center_window(root, window):
  924. # type: (tk.Tk, tk.Toplevel) -> None
  925. """
  926. Attempts to center window on screen
  927. """
  928. root.update_idletasks()
  929. # root.eval('tk::PlaceWindow {!s} center'.format(
  930. # window.winfo_pathname(window.winfo_id())
  931. # ))
  932. w = window.winfo_width()
  933. h = window.winfo_height()
  934. ws = window.winfo_screenwidth()
  935. hs = window.winfo_screenheight()
  936. x = (ws / 2) - (w / 2)
  937. y = (hs / 2) - (h / 2)
  938. window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
  939. window.lift()
  940. window.focus_force()
  941. def _center_window_above_parent(root, window):
  942. # type: (tk.Tk, tk.Toplevel) -> None
  943. """
  944. Attempts to center window above its parent window
  945. """
  946. # root.eval('tk::PlaceWindow {!s} center'.format(
  947. # window.winfo_pathname(window.winfo_id())
  948. # ))
  949. root.update_idletasks()
  950. parent = window.master
  951. w = window.winfo_width()
  952. h = window.winfo_height()
  953. px = parent.winfo_rootx()
  954. py = parent.winfo_rooty()
  955. pw = parent.winfo_width()
  956. ph = parent.winfo_height()
  957. x = px + (pw / 2) - (w / 2)
  958. y = py + (ph / 2) - (h / 2)
  959. window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
  960. window.lift()
  961. window.focus_force()
  962. def main(argv=None):
  963. if argv is None:
  964. argv = sys.argv[1:]
  965. # Instantiate cmd options parser
  966. parser = argparse.ArgumentParser(
  967. description='Interactive Kconfig configuration editor'
  968. )
  969. parser.add_argument(
  970. '--kconfig',
  971. metavar='FILE',
  972. type=str,
  973. default='Kconfig',
  974. help='path to root Kconfig file'
  975. )
  976. parser.add_argument(
  977. '--config',
  978. metavar='FILE',
  979. type=str,
  980. help='path to .config file to load'
  981. )
  982. args = parser.parse_args(argv)
  983. kconfig_path = args.kconfig
  984. config_path = args.config
  985. # Verify that Kconfig file exists
  986. if not os.path.isfile(kconfig_path):
  987. raise RuntimeError('\'{}\': no such file'.format(kconfig_path))
  988. # Parse Kconfig files
  989. kconf = kconfiglib.Kconfig(filename=kconfig_path)
  990. mc = MenuConfig(kconf)
  991. # If config file was specified, load it
  992. if config_path:
  993. mc.open_config(config_path)
  994. tk.mainloop()
  995. if __name__ == '__main__':
  996. main()