pyguiconfig.py 76 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315
  1. #!/usr/bin/env python
  2. # Copyright (c) 2019, Ulf Magnusson
  3. # SPDX-License-Identifier: ISC
  4. """
  5. Overview
  6. ========
  7. A Tkinter-based menuconfig implementation, based around a treeview control and
  8. a help display. The interface should feel familiar to people used to qconf
  9. ('make xconfig'). Compatible with both Python 2 and Python 3.
  10. The display can be toggled between showing the full tree and showing just a
  11. single menu (like menuconfig.py). Only single-menu mode distinguishes between
  12. symbols defined with 'config' and symbols defined with 'menuconfig'.
  13. A show-all mode is available that shows invisible items in red.
  14. Supports both mouse and keyboard controls. The following keyboard shortcuts are
  15. available:
  16. Ctrl-S : Save configuration
  17. Ctrl-O : Open configuration
  18. Ctrl-A : Toggle show-all mode
  19. Ctrl-N : Toggle show-name mode
  20. Ctrl-M : Toggle single-menu mode
  21. Ctrl-F, /: Open jump-to dialog
  22. ESC : Close
  23. Running
  24. =======
  25. guiconfig.py can be run either as a standalone executable or by calling the
  26. menuconfig() function with an existing Kconfig instance. The second option is a
  27. bit inflexible in that it will still load and save .config, etc.
  28. When run in standalone mode, the top-level Kconfig file to load can be passed
  29. as a command-line argument. With no argument, it defaults to "Kconfig".
  30. The KCONFIG_CONFIG environment variable specifies the .config file to load (if
  31. it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used.
  32. When overwriting a configuration file, the old version is saved to
  33. <filename>.old (e.g. .config.old).
  34. $srctree is supported through Kconfiglib.
  35. """
  36. # Note: There's some code duplication with menuconfig.py below, especially for
  37. # the help text. Maybe some of it could be moved into kconfiglib.py or a shared
  38. # helper script, but OTOH it's pretty nice to have things standalone and
  39. # customizable.
  40. import errno
  41. import os
  42. import sys
  43. _PY2 = sys.version_info[0] < 3
  44. if _PY2:
  45. # Python 2
  46. from Tkinter import *
  47. import ttk
  48. import tkFont as font
  49. import tkFileDialog as filedialog
  50. import tkMessageBox as messagebox
  51. else:
  52. # Python 3
  53. from tkinter import *
  54. import tkinter.ttk as ttk
  55. import tkinter.font as font
  56. from tkinter import filedialog, messagebox
  57. from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \
  58. BOOL, TRISTATE, STRING, INT, HEX, \
  59. AND, OR, \
  60. expr_str, expr_value, split_expr, \
  61. standard_sc_expr_str, \
  62. TRI_TO_STR, TYPE_TO_STR, \
  63. standard_kconfig, standard_config_filename
  64. # If True, use GIF image data embedded in this file instead of separate GIF
  65. # files. See _load_images().
  66. _USE_EMBEDDED_IMAGES = True
  67. # Help text for the jump-to dialog
  68. _JUMP_TO_HELP = """\
  69. Type one or more strings/regexes and press Enter to list items that match all
  70. of them. Python's regex flavor is used (see the 're' module). Double-clicking
  71. an item will jump to it. Item values can be toggled directly within the dialog.\
  72. """
  73. def _main():
  74. menuconfig(standard_kconfig())
  75. # Global variables used below:
  76. #
  77. # _root:
  78. # The Toplevel instance for the main window
  79. #
  80. # _tree:
  81. # The Treeview in the main window
  82. #
  83. # _jump_to_tree:
  84. # The Treeview in the jump-to dialog. None if the jump-to dialog isn't
  85. # open. Doubles as a flag.
  86. #
  87. # _jump_to_matches:
  88. # List of Nodes shown in the jump-to dialog
  89. #
  90. # _menupath:
  91. # The Label that shows the menu path of the selected item
  92. #
  93. # _backbutton:
  94. # The button shown in single-menu mode for jumping to the parent menu
  95. #
  96. # _status_label:
  97. # Label with status text shown at the bottom of the main window
  98. # ("Modified", "Saved to ...", etc.)
  99. #
  100. # _id_to_node:
  101. # We can't use Node objects directly as Treeview item IDs, so we use their
  102. # id()s instead. This dictionary maps Node id()s back to Nodes. (The keys
  103. # are actually str(id(node)), just to simplify lookups.)
  104. #
  105. # _cur_menu:
  106. # The current menu. Ignored outside single-menu mode.
  107. #
  108. # _show_all_var/_show_name_var/_single_menu_var:
  109. # Tkinter Variable instances bound to the corresponding checkboxes
  110. #
  111. # _show_all/_single_menu:
  112. # Plain Python bools that track _show_all_var and _single_menu_var, to
  113. # speed up and simplify things a bit
  114. #
  115. # _conf_filename:
  116. # File to save the configuration to
  117. #
  118. # _minconf_filename:
  119. # File to save minimal configurations to
  120. #
  121. # _conf_changed:
  122. # True if the configuration has been changed. If False, we don't bother
  123. # showing the save-and-quit dialog.
  124. #
  125. # We reset this to False whenever the configuration is saved.
  126. #
  127. # _*_img:
  128. # PhotoImage instances for images
  129. def menuconfig(kconf):
  130. """
  131. Launches the configuration interface, returning after the user exits.
  132. kconf:
  133. Kconfig instance to be configured
  134. """
  135. global _kconf
  136. global _conf_filename
  137. global _minconf_filename
  138. global _jump_to_tree
  139. global _cur_menu
  140. _kconf = kconf
  141. _jump_to_tree = None
  142. _create_id_to_node()
  143. _create_ui()
  144. # Filename to save configuration to
  145. _conf_filename = standard_config_filename()
  146. # Load existing configuration and check if it's outdated
  147. _set_conf_changed(_load_config())
  148. # Filename to save minimal configuration to
  149. _minconf_filename = "defconfig"
  150. # Current menu in single-menu mode
  151. _cur_menu = _kconf.top_node
  152. # Any visible items in the top menu?
  153. if not _shown_menu_nodes(kconf.top_node):
  154. # Nothing visible. Start in show-all mode and try again.
  155. _show_all_var.set(True)
  156. if not _shown_menu_nodes(kconf.top_node):
  157. # Give up and show an error. It's nice to be able to assume that
  158. # the tree is non-empty in the rest of the code.
  159. _root.wait_visibility()
  160. messagebox.showerror(
  161. "Error",
  162. "Empty configuration -- nothing to configure.\n\n"
  163. "Check that environment variables are set properly.")
  164. _root.destroy()
  165. return
  166. # Build the initial tree
  167. _update_tree()
  168. # Select the first item and focus the Treeview, so that keyboard controls
  169. # work immediately
  170. _select(_tree, _tree.get_children()[0])
  171. _tree.focus_set()
  172. # Make geometry information available for centering the window. This
  173. # indirectly creates the window, so hide it so that it's never shown at the
  174. # old location.
  175. _root.withdraw()
  176. _root.update_idletasks()
  177. # Center the window
  178. _root.geometry("+{}+{}".format(
  179. (_root.winfo_screenwidth() - _root.winfo_reqwidth())//2,
  180. (_root.winfo_screenheight() - _root.winfo_reqheight())//2))
  181. # Show it
  182. _root.deiconify()
  183. # Prevent the window from being automatically resized. Otherwise, it
  184. # changes size when scrollbars appear/disappear before the user has
  185. # manually resized it.
  186. _root.geometry(_root.geometry())
  187. _root.mainloop()
  188. def _load_config():
  189. # Loads any existing .config file. See the Kconfig.load_config() docstring.
  190. #
  191. # Returns True if .config is missing or outdated. We always prompt for
  192. # saving the configuration in that case.
  193. print(_kconf.load_config())
  194. if not os.path.exists(_conf_filename):
  195. # No .config
  196. return True
  197. return _needs_save()
  198. def _needs_save():
  199. # Returns True if a just-loaded .config file is outdated (would get
  200. # modified when saving)
  201. if _kconf.missing_syms:
  202. # Assignments to undefined symbols in the .config
  203. return True
  204. for sym in _kconf.unique_defined_syms:
  205. if sym.user_value is None:
  206. if sym.config_string:
  207. # Unwritten symbol
  208. return True
  209. elif sym.orig_type in (BOOL, TRISTATE):
  210. if sym.tri_value != sym.user_value:
  211. # Written bool/tristate symbol, new value
  212. return True
  213. elif sym.str_value != sym.user_value:
  214. # Written string/int/hex symbol, new value
  215. return True
  216. # No need to prompt for save
  217. return False
  218. def _create_id_to_node():
  219. global _id_to_node
  220. _id_to_node = {str(id(node)): node for node in _kconf.node_iter()}
  221. def _create_ui():
  222. # Creates the main window UI
  223. global _root
  224. global _tree
  225. # Create the root window. This initializes Tkinter and makes e.g.
  226. # PhotoImage available, so do it early.
  227. _root = Tk()
  228. _load_images()
  229. _init_misc_ui()
  230. _fix_treeview_issues()
  231. _create_top_widgets()
  232. # Create the pane with the Kconfig tree and description text
  233. panedwindow, _tree = _create_kconfig_tree_and_desc(_root)
  234. panedwindow.grid(column=0, row=1, sticky="nsew")
  235. _create_status_bar()
  236. _root.columnconfigure(0, weight=1)
  237. # Only the pane with the Kconfig tree and description grows vertically
  238. _root.rowconfigure(1, weight=1)
  239. # Start with show-name disabled
  240. _do_showname()
  241. _tree.bind("<Left>", _tree_left_key)
  242. _tree.bind("<Right>", _tree_right_key)
  243. # Note: Binding this for the jump-to tree as well would cause issues due to
  244. # the Tk bug mentioned in _tree_open()
  245. _tree.bind("<<TreeviewOpen>>", _tree_open)
  246. # add=True to avoid overriding the description text update
  247. _tree.bind("<<TreeviewSelect>>", _update_menu_path, add=True)
  248. _root.bind("<Control-s>", _save)
  249. _root.bind("<Control-o>", _open)
  250. _root.bind("<Control-a>", _toggle_showall)
  251. _root.bind("<Control-n>", _toggle_showname)
  252. _root.bind("<Control-m>", _toggle_tree_mode)
  253. _root.bind("<Control-f>", _jump_to_dialog)
  254. _root.bind("/", _jump_to_dialog)
  255. _root.bind("<Escape>", _on_quit)
  256. def _load_images():
  257. # Loads GIF images, creating the global _*_img PhotoImage variables.
  258. # Base64-encoded images embedded in this script are used if
  259. # _USE_EMBEDDED_IMAGES is True, and separate image files in the same
  260. # directory as the script otherwise.
  261. #
  262. # Using a global variable indirectly prevents the image from being
  263. # garbage-collected. Passing an image to a Tkinter function isn't enough to
  264. # keep it alive.
  265. def load_image(name, data):
  266. var_name = "_{}_img".format(name)
  267. if _USE_EMBEDDED_IMAGES:
  268. globals()[var_name] = PhotoImage(data=data, format="gif")
  269. else:
  270. globals()[var_name] = PhotoImage(
  271. file=os.path.join(os.path.dirname(__file__), name + ".gif"),
  272. format="gif")
  273. # Note: Base64 data can be put on the clipboard with
  274. # $ base64 -w0 foo.gif | xclip
  275. load_image("icon", "R0lGODlhIwAjAPcAAAAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCqmQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPVmTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYrM2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlVM5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq/5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyAM8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8rmf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+qM/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAACH5BAEAAPwALAAAAAAjACMAAAj/AKPtE0hwoMGCAiUF28VwoaJIyg4a3LeMIkVly5QR00gsk8eNvBZGYrhr4SJhmixWJFgPkyReIyVFisTrEi9eu2aOXBhsZE5dkYYtK1gR00lhydoha5dMGFOkwhr6bEhSUTKLApVFEjZsHtdkw56CjRoJ0iJIOYHlJLlL0kqKmXZ1RdoO6bCkw+QJ86nrLK2duy7tAhpx6D5Jwujenat02DBhZx/2LSu54UmB+zTtsrt06eKkyKKSjNT34UO0gyOt1DpvGLvFSF/Lc7yXIVqdihYpyo120bCEXJHNbQ1WeF5JqW2fXbQITBjTkH5H45W4ujCFMGHOXOSTZ8iQwRRu/80ZLdr4tSPTo2e7ViT7YNfpHW4nmKQuhvfZily4NjDD+nsNtRlDPtXXU0mkTVXSew0Jw0tEkgxj4GgL6rfgTpHwN1JikigTzYAILqghTz4BRlJ9JTkI4TD88Uegi1S1WCFPm3X4oWgH9iSjYC4eWOGCie1SEWKiKRIGGLql11xzRx7JnHNQhsGQMJh4SJ1IYGzhnBY2LBLMllmCgSSUWjoZDHxCHuZgMLxoAUaGi2g5mps0XQiGFlNRZ6NcPMEgZU5+btZTGFsAyVAYWkR1JppDsriZFnguYiaBbtpEI6IZfkelh3zmFAYMYMTwJptoImqoJFl6x2hLwwiqRRiSauRx0oWvjifMQp+GFNU613Ea5C43gFESpqTy4pyIIYkZ0mZBDkXdrcK8ulkkhKYYiZY4QbsLorqmaCMvLIYkLa5IihZGGGgqemRUt7IT1TItuQufrVFhB5+Bwqzz3ZT6wmdjUAvdKuiit5KamMHwJfwsOxAd5q4w5eQbFTu8sLOZOcFYrDGbFq/JzpnvTsfVw4mRTLJ1JadcncWYDDVMJMiwk9TMeTHmlDDuPvxwxkEGtQ9cvLjUFiZtIScJ0S5dd13QNL0kU9CZYEYQRtFQTU80y0CzzDL6ZFR11ltrLfbW+9QTTUAAOw==")
  276. load_image("n_bool", "R0lGODlhDgAOAHAAACH5BAEAAPwALAAAAAAOAA4AhwAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCqmQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPVmTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYrM2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlVM5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq/5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyAM8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8rmf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+qM/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAgxAAEIHEiw4L6DCBMeFKiw4T6GDhNCjLgQAEWEEylmjLjRYceGHxWGlGjx4sOCKAcGBAA7")
  277. load_image("y_bool", "R0lGODlhDwAPAHAAACH5BAEAAPwALAAAAAAPAA8AhwAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCqmQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPVmTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYrM2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlVM5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq/5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyAM8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8rmf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+qM/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAhLAAEIHEiwIIB9CBMqTChwoUOEDR8iJHZjX0SJDS86HGjx4MGFBiJm/IhwxcWRB5WFJNkRosAYGlvuq0dwI0llMV1KXJjzocGfAwMCADs=")
  278. load_image("n_tri", "R0lGODlhEAAQAPD/AAEBAf///yH5BAUKAAIALAAAAAAQABAAAAInlI+pBrAKQnCPSUlXvFhznlkfeGwjKZhnJ65h6nrfi6h0st2QXikFADs=")
  279. load_image("m_tri", "R0lGODlhEAAQAPEDAAEBAeQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nI+pBrAWAhPCjYhiAJQCnWmdoElHGVBoiK5M21ofXFpXRIrgiecqxkuNciZIhNOZFRNI24PhfEoLADs=")
  280. load_image("y_tri", "R0lGODlhEAAQAPEDAAICAgDQAP///wAAACH5BAUKAAMALAAAAAAQABAAAAI0nI+pBrAYBhDCRRUypfmergmgZ4xjMpmaw2zmxk7cCB+pWiVqp4MzDwn9FhGZ5WFjIZeGAgA7")
  281. load_image("m_my", "R0lGODlhEAAQAPEDAAAAAOQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nIGpxiAPI2ghxFinq/ZygQhc94zgZopmOLYf67anGr+oZdp02emfV5n9MEHN5QhqICETxkABbQ4KADs=")
  282. load_image("y_my", "R0lGODlhEAAQAPH/AAAAAADQAAPRA////yH5BAUKAAQALAAAAAAQABAAAAM+SArcrhCMSSuIM9Q8rxxBWIXawIBkmWonupLd565Um9G1PIs59fKmzw8WnAlusBYR2SEIN6DmAmqBLBxYSAIAOw==")
  283. load_image("n_locked", "R0lGODlhEAAQAPABAAAAAP///yH5BAUKAAEALAAAAAAQABAAAAIgjB8AyKwN04pu0vMutpqqz4Hih4ydlnUpyl2r23pxUAAAOw==")
  284. load_image("m_locked", "R0lGODlhEAAQAPD/AAAAAOQMuiH5BAUKAAIALAAAAAAQABAAAAIylC8AyKwN04ohnGcqqlZmfXDWI26iInZoyiore05walolV39ftxsYHgL9QBBMBGFEFAAAOw==")
  285. load_image("y_locked", "R0lGODlhDwAPAHAAACH5BAEAAPwALAAAAAAPAA8AhwAAAAAAMwAAZgAAmQAAzAAA/wArAAArMwArZgArmQArzAAr/wBVAABVMwBVZgBVmQBVzABV/wCAAACAMwCAZgCAmQCAzACA/wCqAACqMwCqZgCqmQCqzACq/wDVAADVMwDVZgDVmQDVzADV/wD/AAD/MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMrADMrMzMrZjMrmTMrzDMr/zNVADNVMzNVZjNVmTNVzDNV/zOAADOAMzOAZjOAmTOAzDOA/zOqADOqMzOqZjOqmTOqzDOq/zPVADPVMzPVZjPVmTPVzDPV/zP/ADP/MzP/ZjP/mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YrAGYrM2YrZmYrmWYrzGYr/2ZVAGZVM2ZVZmZVmWZVzGZV/2aAAGaAM2aAZmaAmWaAzGaA/2aqAGaqM2aqZmaqmWaqzGaq/2bVAGbVM2bVZmbVmWbVzGbV/2b/AGb/M2b/Zmb/mWb/zGb//5kAAJkAM5kAZpkAmZkAzJkA/5krAJkrM5krZpkrmZkrzJkr/5lVAJlVM5lVZplVmZlVzJlV/5mAAJmAM5mAZpmAmZmAzJmA/5mqAJmqM5mqZpmqmZmqzJmq/5nVAJnVM5nVZpnVmZnVzJnV/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwAM8wAZswAmcwAzMwA/8wrAMwrM8wrZswrmcwrzMwr/8xVAMxVM8xVZsxVmcxVzMxV/8yAAMyAM8yAZsyAmcyAzMyA/8yqAMyqM8yqZsyqmcyqzMyq/8zVAMzVM8zVZszVmczVzMzV/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8Amf8AzP8A//8rAP8rM/8rZv8rmf8rzP8r//9VAP9VM/9VZv9Vmf9VzP9V//+AAP+AM/+AZv+Amf+AzP+A//+qAP+qM/+qZv+qmf+qzP+q///VAP/VM//VZv/Vmf/VzP/V////AP//M///Zv//mf//zP///wAAAAAAAAAAAAAAAAg1AAEIHEiwIIB9CBMqTChwoUOEDR8ujCiR4cGKFjFmNFhwX0OOBD1e1EgRY8mKJyWmfAgSZEAAOw==")
  286. load_image("not_selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIrlA2px6IBw2IpWglOvTYhzmUbGD3kNZ5QqrKn2YrqigCxZoMelU6No9gdCgA7")
  287. load_image("selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIzlA2px6IBw2IpWglOvTah/kTZhimASJomiqonlLov1qptHTsgKSEzh9H8QI0QzNPwmRoFADs=")
  288. load_image("edit", "R0lGODlhEAAQAPIFAAAAAKOLAMuuEPvXCvrxvgAAAAAAAAAAACH5BAUKAAUALAAAAAAQABAAAANCWLqw/gqMBp8cszJxcwVC2FEOEIAi5kVBi3IqWZhuCGMyfdpj2e4pnK+WAshmvxeAcETWlsxPkkBtsqBMa8TIBSQAADs=")
  289. def _fix_treeview_issues():
  290. # Fixes some Treeview issues
  291. global _treeview_rowheight
  292. style = ttk.Style()
  293. # The treeview rowheight isn't adjusted automatically on high-DPI displays,
  294. # so do it ourselves. The font will probably always be TkDefaultFont, but
  295. # play it safe and look it up.
  296. _treeview_rowheight = font.Font(font=style.lookup("Treeview", "font")) \
  297. .metrics("linespace") + 2
  298. style.configure("Treeview", rowheight=_treeview_rowheight)
  299. # Work around regression in https://core.tcl.tk/tk/tktview?name=509cafafae,
  300. # which breaks tag background colors
  301. for option in "foreground", "background":
  302. # Filter out any styles starting with ("!disabled", "!selected", ...).
  303. # style.map() returns an empty list for missing options, so this should
  304. # be future-safe.
  305. style.map(
  306. "Treeview",
  307. **{option: [elm for elm in style.map("Treeview", query_opt=option)
  308. if elm[:2] != ("!disabled", "!selected")]})
  309. def _init_misc_ui():
  310. # Does misc. UI initialization, like setting the title, icon, and theme
  311. _root.title(_kconf.mainmenu_text)
  312. # iconphoto() isn't available in Python 2's Tkinter
  313. _root.tk.call("wm", "iconphoto", _root._w, "-default", _icon_img)
  314. # Reducing the width of the window to 1 pixel makes it move around, at
  315. # least on GNOME. Prevent weird stuff like that.
  316. _root.minsize(128, 128)
  317. _root.protocol("WM_DELETE_WINDOW", _on_quit)
  318. # Use the 'clam' theme on *nix if it's available. It looks nicer than the
  319. # 'default' theme.
  320. style = ttk.Style()
  321. style.theme_use("default")
  322. if _root.tk.call("tk", "windowingsystem") == "x11":
  323. if "clam" in style.theme_names():
  324. style.theme_use("clam")
  325. def _create_top_widgets():
  326. # Creates the controls above the Kconfig tree in the main window
  327. global _show_all_var
  328. global _show_name_var
  329. global _single_menu_var
  330. global _menupath
  331. global _backbutton
  332. topframe = ttk.Frame(_root)
  333. topframe.grid(column=0, row=0, sticky="ew")
  334. ttk.Button(topframe, text="Save", command=_save) \
  335. .grid(column=0, row=0, sticky="ew", padx=".05c", pady=".05c")
  336. ttk.Button(topframe, text="Save as...", command=_save_as) \
  337. .grid(column=1, row=0, sticky="ew")
  338. ttk.Button(topframe, text="Save minimal (advanced)...",
  339. command=_save_minimal) \
  340. .grid(column=2, row=0, sticky="ew", padx=".05c")
  341. ttk.Button(topframe, text="Open...", command=_open) \
  342. .grid(column=3, row=0)
  343. ttk.Button(topframe, text="Jump to...", command=_jump_to_dialog) \
  344. .grid(column=4, row=0, padx=".05c")
  345. _show_name_var = BooleanVar()
  346. ttk.Checkbutton(topframe, text="Show name", command=_do_showname,
  347. variable=_show_name_var) \
  348. .grid(column=0, row=1, sticky="nsew", padx=".05c", pady="0 .05c",
  349. ipady=".2c")
  350. _show_all_var = BooleanVar()
  351. ttk.Checkbutton(topframe, text="Show all", command=_do_showall,
  352. variable=_show_all_var) \
  353. .grid(column=1, row=1, sticky="nsew", pady="0 .05c")
  354. # Allow the show-all and single-menu status to be queried via plain global
  355. # Python variables, which is faster and simpler
  356. def show_all_updated(*_):
  357. global _show_all
  358. _show_all = _show_all_var.get()
  359. _trace_write(_show_all_var, show_all_updated)
  360. _show_all_var.set(False)
  361. _single_menu_var = BooleanVar()
  362. ttk.Checkbutton(topframe, text="Single-menu mode", command=_do_tree_mode,
  363. variable=_single_menu_var) \
  364. .grid(column=2, row=1, sticky="nsew", padx=".05c", pady="0 .05c")
  365. _backbutton = ttk.Button(topframe, text="<--", command=_leave_menu,
  366. state="disabled")
  367. _backbutton.grid(column=0, row=4, sticky="nsew", padx=".05c", pady="0 .05c")
  368. def tree_mode_updated(*_):
  369. global _single_menu
  370. _single_menu = _single_menu_var.get()
  371. if _single_menu:
  372. _backbutton.grid()
  373. else:
  374. _backbutton.grid_remove()
  375. _trace_write(_single_menu_var, tree_mode_updated)
  376. _single_menu_var.set(False)
  377. # Column to the right of the buttons that the menu path extends into, so
  378. # that it can grow wider than the buttons
  379. topframe.columnconfigure(5, weight=1)
  380. _menupath = ttk.Label(topframe)
  381. _menupath.grid(column=0, row=3, columnspan=6, sticky="w", padx="0.05c",
  382. pady="0 .05c")
  383. def _create_kconfig_tree_and_desc(parent):
  384. # Creates a Panedwindow with a Treeview that shows Kconfig nodes and a Text
  385. # that shows a description of the selected node. Returns a tuple with the
  386. # Panedwindow and the Treeview. This code is shared between the main window
  387. # and the jump-to dialog.
  388. panedwindow = ttk.Panedwindow(parent, orient=VERTICAL)
  389. tree_frame, tree = _create_kconfig_tree(panedwindow)
  390. desc_frame, desc = _create_kconfig_desc(panedwindow)
  391. panedwindow.add(tree_frame, weight=1)
  392. panedwindow.add(desc_frame)
  393. def tree_select(_):
  394. # The Text widget does not allow editing the text in its disabled
  395. # state. We need to temporarily enable it.
  396. desc["state"] = "normal"
  397. sel = tree.selection()
  398. if not sel:
  399. desc.delete("1.0", "end")
  400. desc["state"] = "disabled"
  401. return
  402. # Text.replace() is not available in Python 2's Tkinter
  403. desc.delete("1.0", "end")
  404. desc.insert("end", _info_str(_id_to_node[sel[0]]))
  405. desc["state"] = "disabled"
  406. tree.bind("<<TreeviewSelect>>", tree_select)
  407. tree.bind("<1>", _tree_click)
  408. tree.bind("<Double-1>", _tree_double_click)
  409. tree.bind("<Return>", _tree_enter)
  410. tree.bind("<KP_Enter>", _tree_enter)
  411. tree.bind("<space>", _tree_toggle)
  412. tree.bind("n", _tree_set_val(0))
  413. tree.bind("m", _tree_set_val(1))
  414. tree.bind("y", _tree_set_val(2))
  415. return panedwindow, tree
  416. def _create_kconfig_tree(parent):
  417. # Creates a Treeview for showing Kconfig nodes
  418. frame = ttk.Frame(parent)
  419. tree = ttk.Treeview(frame, selectmode="browse", height=20,
  420. columns=("name",))
  421. tree.heading("#0", text="Option", anchor="w")
  422. tree.heading("name", text="Name", anchor="w")
  423. tree.tag_configure("n-bool", image=_n_bool_img)
  424. tree.tag_configure("y-bool", image=_y_bool_img)
  425. tree.tag_configure("m-tri", image=_m_tri_img)
  426. tree.tag_configure("n-tri", image=_n_tri_img)
  427. tree.tag_configure("m-tri", image=_m_tri_img)
  428. tree.tag_configure("y-tri", image=_y_tri_img)
  429. tree.tag_configure("m-my", image=_m_my_img)
  430. tree.tag_configure("y-my", image=_y_my_img)
  431. tree.tag_configure("n-locked", image=_n_locked_img)
  432. tree.tag_configure("m-locked", image=_m_locked_img)
  433. tree.tag_configure("y-locked", image=_y_locked_img)
  434. tree.tag_configure("not-selected", image=_not_selected_img)
  435. tree.tag_configure("selected", image=_selected_img)
  436. # tree.tag_configure("edit", image=_edit_img)
  437. tree.tag_configure("invisible", foreground="red")
  438. tree.grid(column=0, row=0, sticky="nsew")
  439. _add_vscrollbar(frame, tree)
  440. frame.columnconfigure(0, weight=1)
  441. frame.rowconfigure(0, weight=1)
  442. # Create items for all menu nodes. These can be detached/moved later.
  443. # Micro-optimize this a bit.
  444. insert = tree.insert
  445. id_ = id
  446. Symbol_ = Symbol
  447. for node in _kconf.node_iter():
  448. item = node.item
  449. insert("", "end", iid=id_(node),
  450. values=item.name if item.__class__ is Symbol_ else "")
  451. return frame, tree
  452. def _create_kconfig_desc(parent):
  453. # Creates a Text for showing the description of the selected Kconfig node
  454. frame = ttk.Frame(parent)
  455. desc = Text(frame, height=12, wrap="none", borderwidth=0,
  456. state="disabled")
  457. desc.grid(column=0, row=0, sticky="nsew")
  458. # Work around not being to Ctrl-C/V text from a disabled Text widget, with a
  459. # tip found in https://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only
  460. desc.bind("<1>", lambda _: desc.focus_set())
  461. _add_vscrollbar(frame, desc)
  462. frame.columnconfigure(0, weight=1)
  463. frame.rowconfigure(0, weight=1)
  464. return frame, desc
  465. def _add_vscrollbar(parent, widget):
  466. # Adds a vertical scrollbar to 'widget' that's only shown as needed
  467. vscrollbar = ttk.Scrollbar(parent, orient="vertical",
  468. command=widget.yview)
  469. vscrollbar.grid(column=1, row=0, sticky="ns")
  470. def yscrollcommand(first, last):
  471. # Only show the scrollbar when needed. 'first' and 'last' are
  472. # strings.
  473. if float(first) <= 0.0 and float(last) >= 1.0:
  474. vscrollbar.grid_remove()
  475. else:
  476. vscrollbar.grid()
  477. vscrollbar.set(first, last)
  478. widget["yscrollcommand"] = yscrollcommand
  479. def _create_status_bar():
  480. # Creates the status bar at the bottom of the main window
  481. global _status_label
  482. _status_label = ttk.Label(_root, anchor="e", padding="0 0 0.4c 0")
  483. _status_label.grid(column=0, row=3, sticky="ew")
  484. def _set_status(s):
  485. # Sets the text in the status bar to 's'
  486. _status_label["text"] = s
  487. def _set_conf_changed(changed):
  488. # Updates the status re. whether there are unsaved changes
  489. global _conf_changed
  490. _conf_changed = changed
  491. if changed:
  492. _set_status("Modified")
  493. def _update_tree():
  494. # Updates the Kconfig tree in the main window by first detaching all nodes
  495. # and then updating and reattaching them. The tree structure might have
  496. # changed.
  497. # If a selected/focused item is detached and later reattached, it stays
  498. # selected/focused. That can give multiple selections even though
  499. # selectmode=browse. Save and later restore the selection and focus as a
  500. # workaround.
  501. old_selection = _tree.selection()
  502. old_focus = _tree.focus()
  503. # Detach all tree items before re-stringing them. This is relatively fast,
  504. # luckily.
  505. _tree.detach(*_id_to_node.keys())
  506. if _single_menu:
  507. _build_menu_tree()
  508. else:
  509. _build_full_tree(_kconf.top_node)
  510. _tree.selection_set(old_selection)
  511. _tree.focus(old_focus)
  512. def _build_full_tree(menu):
  513. # Updates the tree starting from menu.list, in full-tree mode. To speed
  514. # things up, only open menus are updated. The menu-at-a-time logic here is
  515. # to deal with invisible items that can show up outside show-all mode (see
  516. # _shown_full_nodes()).
  517. for node in _shown_full_nodes(menu):
  518. _add_to_tree(node, _kconf.top_node)
  519. # _shown_full_nodes() includes nodes from menus rooted at symbols, so
  520. # we only need to check "real" menus/choices here
  521. if node.list and not isinstance(node.item, Symbol):
  522. if _tree.item(id(node), "open"):
  523. _build_full_tree(node)
  524. else:
  525. # We're just probing here, so _shown_menu_nodes() will work
  526. # fine, and might be a bit faster
  527. shown = _shown_menu_nodes(node)
  528. if shown:
  529. # Dummy element to make the open/closed toggle appear
  530. _tree.move(id(shown[0]), id(shown[0].parent), "end")
  531. def _shown_full_nodes(menu):
  532. # Returns the list of menu nodes shown in 'menu' (a menu node for a menu)
  533. # for full-tree mode. A tricky detail is that invisible items need to be
  534. # shown if they have visible children.
  535. def rec(node):
  536. res = []
  537. while node:
  538. if _visible(node) or _show_all:
  539. res.append(node)
  540. if node.list and isinstance(node.item, Symbol):
  541. # Nodes from menu created from dependencies
  542. res += rec(node.list)
  543. elif node.list and isinstance(node.item, Symbol):
  544. # Show invisible symbols (defined with either 'config' and
  545. # 'menuconfig') if they have visible children. This can happen
  546. # for an m/y-valued symbol with an optional prompt
  547. # ('prompt "foo" is COND') that is currently disabled.
  548. shown_children = rec(node.list)
  549. if shown_children:
  550. res.append(node)
  551. res += shown_children
  552. node = node.next
  553. return res
  554. return rec(menu.list)
  555. def _build_menu_tree():
  556. # Updates the tree in single-menu mode. See _build_full_tree() as well.
  557. for node in _shown_menu_nodes(_cur_menu):
  558. _add_to_tree(node, _cur_menu)
  559. def _shown_menu_nodes(menu):
  560. # Used for single-menu mode. Similar to _shown_full_nodes(), but doesn't
  561. # include children of symbols defined with 'menuconfig'.
  562. def rec(node):
  563. res = []
  564. while node:
  565. if _visible(node) or _show_all:
  566. res.append(node)
  567. if node.list and not node.is_menuconfig:
  568. res += rec(node.list)
  569. elif node.list and isinstance(node.item, Symbol):
  570. shown_children = rec(node.list)
  571. if shown_children:
  572. # Invisible item with visible children
  573. res.append(node)
  574. if not node.is_menuconfig:
  575. res += shown_children
  576. node = node.next
  577. return res
  578. return rec(menu.list)
  579. def _visible(node):
  580. # Returns True if the node should appear in the menu (outside show-all
  581. # mode)
  582. return node.prompt and expr_value(node.prompt[1]) and not \
  583. (node.item == MENU and not expr_value(node.visibility))
  584. def _add_to_tree(node, top):
  585. # Adds 'node' to the tree, at the end of its menu. We rely on going through
  586. # the nodes linearly to get the correct order. 'top' holds the menu that
  587. # corresponds to the top-level menu, and can vary in single-menu mode.
  588. parent = node.parent
  589. _tree.move(id(node), "" if parent is top else id(parent), "end")
  590. _tree.item(
  591. id(node),
  592. text=_node_str(node),
  593. # The _show_all test avoids showing invisible items in red outside
  594. # show-all mode, which could look confusing/broken. Invisible symbols
  595. # are shown outside show-all mode if an invisible symbol has visible
  596. # children in an implicit menu.
  597. tags=_img_tag(node) if _visible(node) or not _show_all else
  598. _img_tag(node) + " invisible")
  599. def _node_str(node):
  600. # Returns the string shown to the right of the image (if any) for the node
  601. if node.prompt:
  602. if node.item == COMMENT:
  603. s = "*** {} ***".format(node.prompt[0])
  604. else:
  605. s = node.prompt[0]
  606. if isinstance(node.item, Symbol):
  607. sym = node.item
  608. # Print "(NEW)" next to symbols without a user value (from e.g. a
  609. # .config), but skip it for choice symbols in choices in y mode,
  610. # and for symbols of UNKNOWN type (which generate a warning though)
  611. if sym.user_value is None and sym.type and not \
  612. (sym.choice and sym.choice.tri_value == 2):
  613. s += " (NEW)"
  614. elif isinstance(node.item, Symbol):
  615. # Symbol without prompt (can show up in show-all)
  616. s = "<{}>".format(node.item.name)
  617. else:
  618. # Choice without prompt. Use standard_sc_expr_str() so that it shows up
  619. # as '<choice (name if any)>'.
  620. s = standard_sc_expr_str(node.item)
  621. if isinstance(node.item, Symbol):
  622. sym = node.item
  623. if sym.orig_type == STRING:
  624. s += ": " + sym.str_value
  625. elif sym.orig_type in (INT, HEX):
  626. s = "({}) {}".format(sym.str_value, s)
  627. elif isinstance(node.item, Choice) and node.item.tri_value == 2:
  628. # Print the prompt of the selected symbol after the choice for
  629. # choices in y mode
  630. sym = node.item.selection
  631. if sym:
  632. for sym_node in sym.nodes:
  633. # Use the prompt used at this choice location, in case the
  634. # choice symbol is defined in multiple locations
  635. if sym_node.parent is node and sym_node.prompt:
  636. s += " ({})".format(sym_node.prompt[0])
  637. break
  638. else:
  639. # If the symbol isn't defined at this choice location, then
  640. # just use whatever prompt we can find for it
  641. for sym_node in sym.nodes:
  642. if sym_node.prompt:
  643. s += " ({})".format(sym_node.prompt[0])
  644. break
  645. # In single-menu mode, print "--->" next to nodes that have menus that can
  646. # potentially be entered. Print "----" if the menu is empty. We don't allow
  647. # those to be entered.
  648. if _single_menu and node.is_menuconfig:
  649. s += " --->" if _shown_menu_nodes(node) else " ----"
  650. return s
  651. def _img_tag(node):
  652. # Returns the tag for the image that should be shown next to 'node', or the
  653. # empty string if it shouldn't have an image
  654. item = node.item
  655. if item in (MENU, COMMENT) or not item.orig_type:
  656. return ""
  657. if item.orig_type in (STRING, INT, HEX):
  658. return "edit"
  659. # BOOL or TRISTATE
  660. if _is_y_mode_choice_sym(item):
  661. # Choice symbol in y-mode choice
  662. return "selected" if item.choice.selection is item else "not-selected"
  663. if len(item.assignable) <= 1:
  664. # Pinned to a single value
  665. return "" if isinstance(item, Choice) else item.str_value + "-locked"
  666. if item.type == BOOL:
  667. return item.str_value + "-bool"
  668. # item.type == TRISTATE
  669. if item.assignable == (1, 2):
  670. return item.str_value + "-my"
  671. return item.str_value + "-tri"
  672. def _is_y_mode_choice_sym(item):
  673. # The choice mode is an upper bound on the visibility of choice symbols, so
  674. # we can check the choice symbols' own visibility to see if the choice is
  675. # in y mode
  676. return isinstance(item, Symbol) and item.choice and item.visibility == 2
  677. def _tree_click(event):
  678. # Click on the Kconfig Treeview
  679. tree = event.widget
  680. if tree.identify_element(event.x, event.y) == "image":
  681. item = tree.identify_row(event.y)
  682. # Select the item before possibly popping up a dialog for
  683. # string/int/hex items, so that its help is visible
  684. _select(tree, item)
  685. _change_node(_id_to_node[item], tree.winfo_toplevel())
  686. return "break"
  687. def _tree_double_click(event):
  688. # Double-click on the Kconfig treeview
  689. # Do an extra check to avoid weirdness when double-clicking in the tree
  690. # heading area
  691. if not _in_heading(event):
  692. return _tree_enter(event)
  693. def _in_heading(event):
  694. # Returns True if 'event' took place in the tree heading
  695. tree = event.widget
  696. return hasattr(tree, "identify_region") and \
  697. tree.identify_region(event.x, event.y) in ("heading", "separator")
  698. def _tree_enter(event):
  699. # Enter press or double-click within the Kconfig treeview. Prefer to
  700. # open/close/enter menus, but toggle the value if that's not possible.
  701. tree = event.widget
  702. sel = tree.focus()
  703. if sel:
  704. node = _id_to_node[sel]
  705. if tree.get_children(sel):
  706. _tree_toggle_open(sel)
  707. elif _single_menu_mode_menu(node, tree):
  708. _enter_menu_and_select_first(node)
  709. else:
  710. _change_node(node, tree.winfo_toplevel())
  711. return "break"
  712. def _tree_toggle(event):
  713. # Space press within the Kconfig treeview. Prefer to toggle the value, but
  714. # open/close/enter the menu if that's not possible.
  715. tree = event.widget
  716. sel = tree.focus()
  717. if sel:
  718. node = _id_to_node[sel]
  719. if _changeable(node):
  720. _change_node(node, tree.winfo_toplevel())
  721. elif _single_menu_mode_menu(node, tree):
  722. _enter_menu_and_select_first(node)
  723. elif tree.get_children(sel):
  724. _tree_toggle_open(sel)
  725. return "break"
  726. def _tree_left_key(_):
  727. # Left arrow key press within the Kconfig treeview
  728. if _single_menu:
  729. # Leave the current menu in single-menu mode
  730. _leave_menu()
  731. return "break"
  732. # Otherwise, default action
  733. def _tree_right_key(_):
  734. # Right arrow key press within the Kconfig treeview
  735. sel = _tree.focus()
  736. if sel:
  737. node = _id_to_node[sel]
  738. # If the node can be entered in single-menu mode, do it
  739. if _single_menu_mode_menu(node, _tree):
  740. _enter_menu_and_select_first(node)
  741. return "break"
  742. # Otherwise, default action
  743. def _single_menu_mode_menu(node, tree):
  744. # Returns True if single-menu mode is on and 'node' is an (interface)
  745. # menu that can be entered
  746. return _single_menu and tree is _tree and node.is_menuconfig and \
  747. _shown_menu_nodes(node)
  748. def _changeable(node):
  749. # Returns True if 'node' is a Symbol/Choice whose value can be changed
  750. sc = node.item
  751. if not isinstance(sc, (Symbol, Choice)):
  752. return False
  753. # This will hit for invisible symbols, which appear in show-all mode and
  754. # when an invisible symbol has visible children (which can happen e.g. for
  755. # symbols with optional prompts)
  756. if not (node.prompt and expr_value(node.prompt[1])):
  757. return False
  758. return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \
  759. or _is_y_mode_choice_sym(sc)
  760. def _tree_toggle_open(item):
  761. # Opens/closes the Treeview item 'item'
  762. if _tree.item(item, "open"):
  763. _tree.item(item, open=False)
  764. else:
  765. node = _id_to_node[item]
  766. if not isinstance(node.item, Symbol):
  767. # Can only get here in full-tree mode
  768. _build_full_tree(node)
  769. _tree.item(item, open=True)
  770. def _tree_set_val(tri_val):
  771. def tree_set_val(event):
  772. # n/m/y press within the Kconfig treeview
  773. # Sets the value of the currently selected item to 'tri_val', if that
  774. # value can be assigned
  775. sel = event.widget.focus()
  776. if sel:
  777. sc = _id_to_node[sel].item
  778. if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable:
  779. _set_val(sc, tri_val)
  780. return tree_set_val
  781. def _tree_open(_):
  782. # Lazily populates the Kconfig tree when menus are opened in full-tree mode
  783. if _single_menu:
  784. # Work around https://core.tcl.tk/tk/tktview?name=368fa4561e
  785. # ("ttk::treeview open/closed indicators can be toggled while hidden").
  786. # Clicking on the hidden indicator will call _build_full_tree() in
  787. # single-menu mode otherwise.
  788. return
  789. node = _id_to_node[_tree.focus()]
  790. # _shown_full_nodes() includes nodes from menus rooted at symbols, so we
  791. # only need to check "real" menus and choices here
  792. if not isinstance(node.item, Symbol):
  793. _build_full_tree(node)
  794. def _update_menu_path(_):
  795. # Updates the displayed menu path when nodes are selected in the Kconfig
  796. # treeview
  797. sel = _tree.selection()
  798. _menupath["text"] = _menu_path_info(_id_to_node[sel[0]]) if sel else ""
  799. def _item_row(item):
  800. # Returns the row number 'item' appears on within the Kconfig treeview,
  801. # starting from the top of the tree. Used to preserve scrolling.
  802. #
  803. # ttkTreeview.c in the Tk sources defines a RowNumber() function that does
  804. # the same thing, but it's not exposed.
  805. row = 0
  806. while True:
  807. prev = _tree.prev(item)
  808. if prev:
  809. item = prev
  810. row += _n_rows(item)
  811. else:
  812. item = _tree.parent(item)
  813. if not item:
  814. return row
  815. row += 1
  816. def _n_rows(item):
  817. # _item_row() helper. Returns the number of rows occupied by 'item' and #
  818. # its children.
  819. rows = 1
  820. if _tree.item(item, "open"):
  821. for child in _tree.get_children(item):
  822. rows += _n_rows(child)
  823. return rows
  824. def _attached(item):
  825. # Heuristic for checking if a Treeview item is attached. Doesn't seem to be
  826. # good APIs for this. Might fail for super-obscure cases with tiny trees,
  827. # but you'd just get a small scroll mess-up.
  828. return bool(_tree.next(item) or _tree.prev(item) or _tree.parent(item))
  829. def _change_node(node, parent):
  830. # Toggles/changes the value of 'node'. 'parent' is the parent window
  831. # (either the main window or the jump-to dialog), in case we need to pop up
  832. # a dialog.
  833. if not _changeable(node):
  834. return
  835. # sc = symbol/choice
  836. sc = node.item
  837. if sc.type in (INT, HEX, STRING):
  838. s = _set_val_dialog(node, parent)
  839. # Tkinter can return 'unicode' strings on Python 2, which Kconfiglib
  840. # can't deal with. UTF-8-encode the string to work around it.
  841. if _PY2 and isinstance(s, unicode):
  842. s = s.encode("utf-8", "ignore")
  843. if s is not None:
  844. _set_val(sc, s)
  845. elif len(sc.assignable) == 1:
  846. # Handles choice symbols for choices in y mode, which are a special
  847. # case: .assignable can be (2,) while .tri_value is 0.
  848. _set_val(sc, sc.assignable[0])
  849. else:
  850. # Set the symbol to the value after the current value in
  851. # sc.assignable, with wrapping
  852. val_index = sc.assignable.index(sc.tri_value)
  853. _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)])
  854. def _set_val(sc, val):
  855. # Wrapper around Symbol/Choice.set_value() for updating the menu state and
  856. # _conf_changed
  857. # Use the string representation of tristate values. This makes the format
  858. # consistent for all symbol types.
  859. if val in TRI_TO_STR:
  860. val = TRI_TO_STR[val]
  861. if val != sc.str_value:
  862. sc.set_value(val)
  863. _set_conf_changed(True)
  864. # Update the tree and try to preserve the scroll. Do a cheaper variant
  865. # than in the show-all case, that might mess up the scroll slightly in
  866. # rare cases, but is fast and flicker-free.
  867. stayput = _loc_ref_item() # Item to preserve scroll for
  868. old_row = _item_row(stayput)
  869. _update_tree()
  870. # If the reference item disappeared (can happen if the change was done
  871. # from the jump-to dialog), then avoid messing with the scroll and hope
  872. # for the best
  873. if _attached(stayput):
  874. _tree.yview_scroll(_item_row(stayput) - old_row, "units")
  875. if _jump_to_tree:
  876. _update_jump_to_display()
  877. def _set_val_dialog(node, parent):
  878. # Pops up a dialog for setting the value of the string/int/hex
  879. # symbol at node 'node'. 'parent' is the parent window.
  880. def ok(_=None):
  881. # No 'nonlocal' in Python 2
  882. global _entry_res
  883. s = entry.get()
  884. if sym.type == HEX and not s.startswith(("0x", "0X")):
  885. s = "0x" + s
  886. if _check_valid(dialog, entry, sym, s):
  887. _entry_res = s
  888. dialog.destroy()
  889. def cancel(_=None):
  890. global _entry_res
  891. _entry_res = None
  892. dialog.destroy()
  893. sym = node.item
  894. dialog = Toplevel(parent)
  895. dialog.title("Enter {} value".format(TYPE_TO_STR[sym.type]))
  896. dialog.resizable(False, False)
  897. dialog.transient(parent)
  898. dialog.protocol("WM_DELETE_WINDOW", cancel)
  899. ttk.Label(dialog, text=node.prompt[0] + ":") \
  900. .grid(column=0, row=0, columnspan=2, sticky="w", padx=".3c",
  901. pady=".2c .05c")
  902. entry = ttk.Entry(dialog, width=30)
  903. # Start with the previous value in the editbox, selected
  904. entry.insert(0, sym.str_value)
  905. entry.selection_range(0, "end")
  906. entry.grid(column=0, row=1, columnspan=2, sticky="ew", padx=".3c")
  907. entry.focus_set()
  908. range_info = _range_info(sym)
  909. if range_info:
  910. ttk.Label(dialog, text=range_info) \
  911. .grid(column=0, row=2, columnspan=2, sticky="w", padx=".3c",
  912. pady=".2c 0")
  913. ttk.Button(dialog, text="OK", command=ok) \
  914. .grid(column=0, row=4 if range_info else 3, sticky="e", padx=".3c",
  915. pady=".4c")
  916. ttk.Button(dialog, text="Cancel", command=cancel) \
  917. .grid(column=1, row=4 if range_info else 3, padx="0 .3c")
  918. # Give all horizontal space to the grid cell with the OK button, so that
  919. # Cancel moves to the right
  920. dialog.columnconfigure(0, weight=1)
  921. _center_on_root(dialog)
  922. # Hack to scroll the entry so that the end of the text is shown, from
  923. # https://stackoverflow.com/questions/29334544/why-does-tkinters-entry-xview-moveto-fail.
  924. # Related Tk ticket: https://core.tcl.tk/tk/info/2513186fff
  925. def scroll_entry(_):
  926. _root.update_idletasks()
  927. entry.unbind("<Expose>")
  928. entry.xview_moveto(1)
  929. entry.bind("<Expose>", scroll_entry)
  930. # The dialog must be visible before we can grab the input
  931. dialog.wait_visibility()
  932. dialog.grab_set()
  933. dialog.bind("<Return>", ok)
  934. dialog.bind("<KP_Enter>", ok)
  935. dialog.bind("<Escape>", cancel)
  936. # Wait for the user to be done with the dialog
  937. parent.wait_window(dialog)
  938. # Regrab the input in the parent
  939. parent.grab_set()
  940. return _entry_res
  941. def _center_on_root(dialog):
  942. # Centers 'dialog' on the root window. It often ends up at some bad place
  943. # like the top-left corner of the screen otherwise. See the menuconfig()
  944. # function, which has similar logic.
  945. dialog.withdraw()
  946. _root.update_idletasks()
  947. dialog_width = dialog.winfo_reqwidth()
  948. dialog_height = dialog.winfo_reqheight()
  949. screen_width = _root.winfo_screenwidth()
  950. screen_height = _root.winfo_screenheight()
  951. x = _root.winfo_rootx() + (_root.winfo_width() - dialog_width)//2
  952. y = _root.winfo_rooty() + (_root.winfo_height() - dialog_height)//2
  953. # Clamp so that no part of the dialog is outside the screen
  954. if x + dialog_width > screen_width:
  955. x = screen_width - dialog_width
  956. elif x < 0:
  957. x = 0
  958. if y + dialog_height > screen_height:
  959. y = screen_height - dialog_height
  960. elif y < 0:
  961. y = 0
  962. dialog.geometry("+{}+{}".format(x, y))
  963. dialog.deiconify()
  964. def _check_valid(dialog, entry, sym, s):
  965. # Returns True if the string 's' is a well-formed value for 'sym'.
  966. # Otherwise, pops up an error and returns False.
  967. if sym.type not in (INT, HEX):
  968. # Anything goes for non-int/hex symbols
  969. return True
  970. base = 10 if sym.type == INT else 16
  971. try:
  972. int(s, base)
  973. except ValueError:
  974. messagebox.showerror(
  975. "Bad value",
  976. "'{}' is a malformed {} value".format(
  977. s, TYPE_TO_STR[sym.type]),
  978. parent=dialog)
  979. entry.focus_set()
  980. return False
  981. for low_sym, high_sym, cond in sym.ranges:
  982. if expr_value(cond):
  983. low_s = low_sym.str_value
  984. high_s = high_sym.str_value
  985. if not int(low_s, base) <= int(s, base) <= int(high_s, base):
  986. messagebox.showerror(
  987. "Value out of range",
  988. "{} is outside the range {}-{}".format(s, low_s, high_s),
  989. parent=dialog)
  990. entry.focus_set()
  991. return False
  992. break
  993. return True
  994. def _range_info(sym):
  995. # Returns a string with information about the valid range for the symbol
  996. # 'sym', or None if 'sym' doesn't have a range
  997. if sym.type in (INT, HEX):
  998. for low, high, cond in sym.ranges:
  999. if expr_value(cond):
  1000. return "Range: {}-{}".format(low.str_value, high.str_value)
  1001. return None
  1002. def _save(_=None):
  1003. # Tries to save the configuration
  1004. if _try_save(_kconf.write_config, _conf_filename, "configuration"):
  1005. _set_conf_changed(False)
  1006. _tree.focus_set()
  1007. def _save_as():
  1008. # Pops up a dialog for saving the configuration to a specific location
  1009. global _conf_filename
  1010. filename = _conf_filename
  1011. while True:
  1012. filename = filedialog.asksaveasfilename(
  1013. title="Save configuration as",
  1014. initialdir=os.path.dirname(filename),
  1015. initialfile=os.path.basename(filename),
  1016. parent=_root)
  1017. if not filename:
  1018. break
  1019. if _try_save(_kconf.write_config, filename, "configuration"):
  1020. _conf_filename = filename
  1021. break
  1022. _tree.focus_set()
  1023. def _save_minimal():
  1024. # Pops up a dialog for saving a minimal configuration (defconfig) to a
  1025. # specific location
  1026. global _minconf_filename
  1027. filename = _minconf_filename
  1028. while True:
  1029. filename = filedialog.asksaveasfilename(
  1030. title="Save minimal configuration as",
  1031. initialdir=os.path.dirname(filename),
  1032. initialfile=os.path.basename(filename),
  1033. parent=_root)
  1034. if not filename:
  1035. break
  1036. if _try_save(_kconf.write_min_config, filename,
  1037. "minimal configuration"):
  1038. _minconf_filename = filename
  1039. break
  1040. _tree.focus_set()
  1041. def _open(_=None):
  1042. # Pops up a dialog for loading a configuration
  1043. global _conf_filename
  1044. if _conf_changed and \
  1045. not messagebox.askokcancel(
  1046. "Unsaved changes",
  1047. "You have unsaved changes. Load new configuration anyway?"):
  1048. return
  1049. filename = _conf_filename
  1050. while True:
  1051. filename = filedialog.askopenfilename(
  1052. title="Open configuration",
  1053. initialdir=os.path.dirname(filename),
  1054. initialfile=os.path.basename(filename),
  1055. parent=_root)
  1056. if not filename:
  1057. break
  1058. if _try_load(filename):
  1059. # Maybe something fancier could be done here later to try to
  1060. # preserve the scroll
  1061. _conf_filename = filename
  1062. _set_conf_changed(_needs_save())
  1063. if _single_menu and not _shown_menu_nodes(_cur_menu):
  1064. # Turn on show-all if we're in single-menu mode and would end
  1065. # up with an empty menu
  1066. _show_all_var.set(True)
  1067. _update_tree()
  1068. break
  1069. _tree.focus_set()
  1070. def _toggle_showname(_):
  1071. # Toggles show-name mode on/off
  1072. _show_name_var.set(not _show_name_var.get())
  1073. _do_showname()
  1074. def _do_showname():
  1075. # Updates the UI for the current show-name setting
  1076. # Columns do not automatically shrink/expand, so we have to update
  1077. # column widths ourselves
  1078. tree_width = _tree.winfo_width()
  1079. if _show_name_var.get():
  1080. _tree["displaycolumns"] = ("name",)
  1081. _tree["show"] = "tree headings"
  1082. name_width = tree_width//3
  1083. _tree.column("#0", width=max(tree_width - name_width, 1))
  1084. _tree.column("name", width=name_width)
  1085. else:
  1086. _tree["displaycolumns"] = ()
  1087. _tree["show"] = "tree"
  1088. _tree.column("#0", width=tree_width)
  1089. _tree.focus_set()
  1090. def _toggle_showall(_):
  1091. # Toggles show-all mode on/off
  1092. _show_all_var.set(not _show_all)
  1093. _do_showall()
  1094. def _do_showall():
  1095. # Updates the UI for the current show-all setting
  1096. # Don't allow turning off show-all if we're in single-menu mode and the
  1097. # current menu would become empty
  1098. if _single_menu and not _shown_menu_nodes(_cur_menu):
  1099. _show_all_var.set(True)
  1100. return
  1101. # Save scroll information. old_scroll can end up negative here, if the
  1102. # reference item isn't shown (only invisible items on the screen, and
  1103. # show-all being turned off).
  1104. stayput = _vis_loc_ref_item()
  1105. # Probe the middle of the first row, to play it safe. identify_row(0) seems
  1106. # to return the row before the top row.
  1107. old_scroll = _item_row(stayput) - \
  1108. _item_row(_tree.identify_row(_treeview_rowheight//2))
  1109. _update_tree()
  1110. if _show_all:
  1111. # Deep magic: Unless we call update_idletasks(), the scroll adjustment
  1112. # below is restricted to the height of the old tree, instead of the
  1113. # height of the new tree. Since the tree with show-all on is guaranteed
  1114. # to be taller, and we want the maximum range, we only call it when
  1115. # turning show-all on.
  1116. #
  1117. # Strictly speaking, something similar ought to be done when changing
  1118. # symbol values, but it causes annoying flicker, and in 99% of cases
  1119. # things work anyway there (with usually minor scroll mess-ups in the
  1120. # 1% case).
  1121. _root.update_idletasks()
  1122. # Restore scroll
  1123. _tree.yview(_item_row(stayput) - old_scroll)
  1124. _tree.focus_set()
  1125. def _toggle_tree_mode(_):
  1126. # Toggles single-menu mode on/off
  1127. _single_menu_var.set(not _single_menu)
  1128. _do_tree_mode()
  1129. def _do_tree_mode():
  1130. # Updates the UI for the current tree mode (full-tree or single-menu)
  1131. loc_ref_node = _id_to_node[_loc_ref_item()]
  1132. if not _single_menu:
  1133. # _jump_to() -> _enter_menu() already updates the tree, but
  1134. # _jump_to() -> load_parents() doesn't, because it isn't always needed.
  1135. # We always need to update the tree here, e.g. to add/remove "--->".
  1136. _update_tree()
  1137. _jump_to(loc_ref_node)
  1138. _tree.focus_set()
  1139. def _enter_menu_and_select_first(menu):
  1140. # Enters the menu 'menu' and selects the first item. Used in single-menu
  1141. # mode.
  1142. _enter_menu(menu)
  1143. _select(_tree, _tree.get_children()[0])
  1144. def _enter_menu(menu):
  1145. # Enters the menu 'menu'. Used in single-menu mode.
  1146. global _cur_menu
  1147. _cur_menu = menu
  1148. _update_tree()
  1149. _backbutton["state"] = "disabled" if menu is _kconf.top_node else "normal"
  1150. def _leave_menu():
  1151. # Leaves the current menu. Used in single-menu mode.
  1152. global _cur_menu
  1153. if _cur_menu is not _kconf.top_node:
  1154. old_menu = _cur_menu
  1155. _cur_menu = _parent_menu(_cur_menu)
  1156. _update_tree()
  1157. _select(_tree, id(old_menu))
  1158. if _cur_menu is _kconf.top_node:
  1159. _backbutton["state"] = "disabled"
  1160. _tree.focus_set()
  1161. def _select(tree, item):
  1162. # Selects, focuses, and see()s 'item' in 'tree'
  1163. tree.selection_set(item)
  1164. tree.focus(item)
  1165. tree.see(item)
  1166. def _loc_ref_item():
  1167. # Returns a Treeview item that can serve as a reference for the current
  1168. # scroll location. We try to make this item stay on the same row on the
  1169. # screen when updating the tree.
  1170. # If the selected item is visible, use that
  1171. sel = _tree.selection()
  1172. if sel and _tree.bbox(sel[0]):
  1173. return sel[0]
  1174. # Otherwise, use the middle item on the screen. If it doesn't exist, the
  1175. # tree is probably really small, so use the first item in the entire tree.
  1176. return _tree.identify_row(_tree.winfo_height()//2) or \
  1177. _tree.get_children()[0]
  1178. def _vis_loc_ref_item():
  1179. # Like _loc_ref_item(), but finds a visible item around the reference item.
  1180. # Used when changing show-all mode, where non-visible (red) items will
  1181. # disappear.
  1182. item = _loc_ref_item()
  1183. vis_before = _vis_before(item)
  1184. if vis_before and _tree.bbox(vis_before):
  1185. return vis_before
  1186. vis_after = _vis_after(item)
  1187. if vis_after and _tree.bbox(vis_after):
  1188. return vis_after
  1189. return vis_before or vis_after
  1190. def _vis_before(item):
  1191. # _vis_loc_ref_item() helper. Returns the first visible (not red) item,
  1192. # searching backwards from 'item'.
  1193. while item:
  1194. if not _tree.tag_has("invisible", item):
  1195. return item
  1196. prev = _tree.prev(item)
  1197. item = prev if prev else _tree.parent(item)
  1198. return None
  1199. def _vis_after(item):
  1200. # _vis_loc_ref_item() helper. Returns the first visible (not red) item,
  1201. # searching forwards from 'item'.
  1202. while item:
  1203. if not _tree.tag_has("invisible", item):
  1204. return item
  1205. next = _tree.next(item)
  1206. if next:
  1207. item = next
  1208. else:
  1209. item = _tree.parent(item)
  1210. if not item:
  1211. break
  1212. item = _tree.next(item)
  1213. return None
  1214. def _on_quit(_=None):
  1215. # Called when the user wants to exit
  1216. if not _conf_changed:
  1217. _quit("No changes to save (for '{}')".format(_conf_filename))
  1218. return
  1219. while True:
  1220. ync = messagebox.askyesnocancel("Quit", "Save changes?")
  1221. if ync is None:
  1222. return
  1223. if not ync:
  1224. _quit("Configuration ({}) was not saved".format(_conf_filename))
  1225. return
  1226. if _try_save(_kconf.write_config, _conf_filename, "configuration"):
  1227. # _try_save() already prints the "Configuration saved to ..."
  1228. # message
  1229. _quit()
  1230. return
  1231. def _quit(msg=None):
  1232. # Quits the application
  1233. # Do not call sys.exit() here, in case we're being run from a script
  1234. _root.destroy()
  1235. if msg:
  1236. print(msg)
  1237. def _try_save(save_fn, filename, description):
  1238. # Tries to save a configuration file. Pops up an error and returns False on
  1239. # failure.
  1240. #
  1241. # save_fn:
  1242. # Function to call with 'filename' to save the file
  1243. #
  1244. # description:
  1245. # String describing the thing being saved
  1246. try:
  1247. # save_fn() returns a message to print
  1248. msg = save_fn(filename)
  1249. _set_status(msg)
  1250. print(msg)
  1251. return True
  1252. except EnvironmentError as e:
  1253. messagebox.showerror(
  1254. "Error saving " + description,
  1255. "Error saving {} to '{}': {} (errno: {})"
  1256. .format(description, e.filename, e.strerror,
  1257. errno.errorcode[e.errno]))
  1258. return False
  1259. def _try_load(filename):
  1260. # Tries to load a configuration file. Pops up an error and returns False on
  1261. # failure.
  1262. #
  1263. # filename:
  1264. # Configuration file to load
  1265. try:
  1266. msg = _kconf.load_config(filename)
  1267. _set_status(msg)
  1268. print(msg)
  1269. return True
  1270. except EnvironmentError as e:
  1271. messagebox.showerror(
  1272. "Error loading configuration",
  1273. "Error loading '{}': {} (errno: {})"
  1274. .format(filename, e.strerror, errno.errorcode[e.errno]))
  1275. return False
  1276. def _jump_to_dialog(_=None):
  1277. # Pops up a dialog for jumping directly to a particular node. Symbol values
  1278. # can also be changed within the dialog.
  1279. #
  1280. # Note: There's nothing preventing this from doing an incremental search
  1281. # like menuconfig.py does, but currently it's a bit jerky for large Kconfig
  1282. # trees, at least when inputting the beginning of the search string. We'd
  1283. # need to somehow only update the tree items that are shown in the Treeview
  1284. # to fix it.
  1285. global _jump_to_tree
  1286. def search(_=None):
  1287. _update_jump_to_matches(msglabel, entry.get())
  1288. def jump_to_selected(event=None):
  1289. # Jumps to the selected node and closes the dialog
  1290. # Ignore double clicks on the image and in the heading area
  1291. if event and (tree.identify_element(event.x, event.y) == "image" or
  1292. _in_heading(event)):
  1293. return
  1294. sel = tree.selection()
  1295. if not sel:
  1296. return
  1297. node = _id_to_node[sel[0]]
  1298. if node not in _shown_menu_nodes(_parent_menu(node)):
  1299. _show_all_var.set(True)
  1300. if not _single_menu:
  1301. # See comment in _do_tree_mode()
  1302. _update_tree()
  1303. _jump_to(node)
  1304. dialog.destroy()
  1305. def tree_select(_):
  1306. jumpto_button["state"] = "normal" if tree.selection() else "disabled"
  1307. dialog = Toplevel(_root)
  1308. dialog.geometry("+{}+{}".format(
  1309. _root.winfo_rootx() + 50, _root.winfo_rooty() + 50))
  1310. dialog.title("Jump to symbol/choice/menu/comment")
  1311. dialog.minsize(128, 128) # See _create_ui()
  1312. dialog.transient(_root)
  1313. ttk.Label(dialog, text=_JUMP_TO_HELP) \
  1314. .grid(column=0, row=0, columnspan=2, sticky="w", padx=".1c",
  1315. pady=".1c")
  1316. entry = ttk.Entry(dialog)
  1317. entry.grid(column=0, row=1, sticky="ew", padx=".1c", pady=".1c")
  1318. entry.focus_set()
  1319. entry.bind("<Return>", search)
  1320. entry.bind("<KP_Enter>", search)
  1321. ttk.Button(dialog, text="Search", command=search) \
  1322. .grid(column=1, row=1, padx="0 .1c", pady="0 .1c")
  1323. msglabel = ttk.Label(dialog)
  1324. msglabel.grid(column=0, row=2, sticky="w", pady="0 .1c")
  1325. panedwindow, tree = _create_kconfig_tree_and_desc(dialog)
  1326. panedwindow.grid(column=0, row=3, columnspan=2, sticky="nsew")
  1327. # Clear tree
  1328. tree.set_children("")
  1329. _jump_to_tree = tree
  1330. jumpto_button = ttk.Button(dialog, text="Jump to selected item",
  1331. state="disabled", command=jump_to_selected)
  1332. jumpto_button.grid(column=0, row=4, columnspan=2, sticky="ns", pady=".1c")
  1333. dialog.columnconfigure(0, weight=1)
  1334. # Only the pane with the Kconfig tree and description grows vertically
  1335. dialog.rowconfigure(3, weight=1)
  1336. # See the menuconfig() function
  1337. _root.update_idletasks()
  1338. dialog.geometry(dialog.geometry())
  1339. # The dialog must be visible before we can grab the input
  1340. dialog.wait_visibility()
  1341. dialog.grab_set()
  1342. tree.bind("<Double-1>", jump_to_selected)
  1343. tree.bind("<Return>", jump_to_selected)
  1344. tree.bind("<KP_Enter>", jump_to_selected)
  1345. # add=True to avoid overriding the description text update
  1346. tree.bind("<<TreeviewSelect>>", tree_select, add=True)
  1347. dialog.bind("<Escape>", lambda _: dialog.destroy())
  1348. # Wait for the user to be done with the dialog
  1349. _root.wait_window(dialog)
  1350. _jump_to_tree = None
  1351. _tree.focus_set()
  1352. def _update_jump_to_matches(msglabel, search_string):
  1353. # Searches for nodes matching the search string and updates
  1354. # _jump_to_matches. Puts a message in 'msglabel' if there are no matches,
  1355. # or regex errors.
  1356. global _jump_to_matches
  1357. _jump_to_tree.selection_set(())
  1358. try:
  1359. # We could use re.IGNORECASE here instead of lower(), but this is
  1360. # faster for regexes like '.*debug$' (though the '.*' is redundant
  1361. # there). Those probably have bad interactions with re.search(), which
  1362. # matches anywhere in the string.
  1363. regex_searches = [re.compile(regex).search
  1364. for regex in search_string.lower().split()]
  1365. except re.error as e:
  1366. msg = "Bad regular expression"
  1367. # re.error.msg was added in Python 3.5
  1368. if hasattr(e, "msg"):
  1369. msg += ": " + e.msg
  1370. msglabel["text"] = msg
  1371. # Clear tree
  1372. _jump_to_tree.set_children("")
  1373. return
  1374. _jump_to_matches = []
  1375. add_match = _jump_to_matches.append
  1376. for node in _sorted_sc_nodes():
  1377. # Symbol/choice
  1378. sc = node.item
  1379. for search in regex_searches:
  1380. # Both the name and the prompt might be missing, since
  1381. # we're searching both symbols and choices
  1382. # Does the regex match either the symbol name or the
  1383. # prompt (if any)?
  1384. if not (sc.name and search(sc.name.lower()) or
  1385. node.prompt and search(node.prompt[0].lower())):
  1386. # Give up on the first regex that doesn't match, to
  1387. # speed things up a bit when multiple regexes are
  1388. # entered
  1389. break
  1390. else:
  1391. add_match(node)
  1392. # Search menus and comments
  1393. for node in _sorted_menu_comment_nodes():
  1394. for search in regex_searches:
  1395. if not search(node.prompt[0].lower()):
  1396. break
  1397. else:
  1398. add_match(node)
  1399. msglabel["text"] = "" if _jump_to_matches else "No matches"
  1400. _update_jump_to_display()
  1401. if _jump_to_matches:
  1402. item = id(_jump_to_matches[0])
  1403. _jump_to_tree.selection_set(item)
  1404. _jump_to_tree.focus(item)
  1405. def _update_jump_to_display():
  1406. # Updates the images and text for the items in _jump_to_matches, and sets
  1407. # them as the items of _jump_to_tree
  1408. # Micro-optimize a bit
  1409. item = _jump_to_tree.item
  1410. id_ = id
  1411. node_str = _node_str
  1412. img_tag = _img_tag
  1413. visible = _visible
  1414. for node in _jump_to_matches:
  1415. item(id_(node),
  1416. text=node_str(node),
  1417. tags=img_tag(node) if visible(node) else
  1418. img_tag(node) + " invisible")
  1419. _jump_to_tree.set_children("", *map(id, _jump_to_matches))
  1420. def _jump_to(node):
  1421. # Jumps directly to 'node' and selects it
  1422. if _single_menu:
  1423. _enter_menu(_parent_menu(node))
  1424. else:
  1425. _load_parents(node)
  1426. _select(_tree, id(node))
  1427. # Obscure Python: We never pass a value for cached_nodes, and it keeps pointing
  1428. # to the same list. This avoids a global.
  1429. def _sorted_sc_nodes(cached_nodes=[]):
  1430. # Returns a sorted list of symbol and choice nodes to search. The symbol
  1431. # nodes appear first, sorted by name, and then the choice nodes, sorted by
  1432. # prompt and (secondarily) name.
  1433. if not cached_nodes:
  1434. # Add symbol nodes
  1435. for sym in sorted(_kconf.unique_defined_syms,
  1436. key=lambda sym: sym.name):
  1437. # += is in-place for lists
  1438. cached_nodes += sym.nodes
  1439. # Add choice nodes
  1440. choices = sorted(_kconf.unique_choices,
  1441. key=lambda choice: choice.name or "")
  1442. cached_nodes += sorted(
  1443. [node
  1444. for choice in choices
  1445. for node in choice.nodes],
  1446. key=lambda node: node.prompt[0] if node.prompt else "")
  1447. return cached_nodes
  1448. def _sorted_menu_comment_nodes(cached_nodes=[]):
  1449. # Returns a list of menu and comment nodes to search, sorted by prompt,
  1450. # with the menus first
  1451. if not cached_nodes:
  1452. def prompt_text(mc):
  1453. return mc.prompt[0]
  1454. cached_nodes += sorted(_kconf.menus, key=prompt_text)
  1455. cached_nodes += sorted(_kconf.comments, key=prompt_text)
  1456. return cached_nodes
  1457. def _load_parents(node):
  1458. # Menus are lazily populated as they're opened in full-tree mode, but
  1459. # jumping to an item needs its parent menus to be populated. This function
  1460. # populates 'node's parents.
  1461. # Get all parents leading up to 'node', sorted with the root first
  1462. parents = []
  1463. cur = node.parent
  1464. while cur is not _kconf.top_node:
  1465. parents.append(cur)
  1466. cur = cur.parent
  1467. parents.reverse()
  1468. for i, parent in enumerate(parents):
  1469. if not _tree.item(id(parent), "open"):
  1470. # Found a closed menu. Populate it and all the remaining menus
  1471. # leading up to 'node'.
  1472. for parent in parents[i:]:
  1473. # We only need to populate "real" menus/choices. Implicit menus
  1474. # are populated when their parents menus are entered.
  1475. if not isinstance(parent.item, Symbol):
  1476. _build_full_tree(parent)
  1477. return
  1478. def _parent_menu(node):
  1479. # Returns the menu node of the menu that contains 'node'. In addition to
  1480. # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'.
  1481. # "Menu" here means a menu in the interface.
  1482. menu = node.parent
  1483. while not menu.is_menuconfig:
  1484. menu = menu.parent
  1485. return menu
  1486. def _trace_write(var, fn):
  1487. # Makes fn() be called whenever the Tkinter Variable 'var' changes value
  1488. # trace_variable() is deprecated according to the docstring,
  1489. # which recommends trace_add()
  1490. if hasattr(var, "trace_add"):
  1491. var.trace_add("write", fn)
  1492. else:
  1493. var.trace_variable("w", fn)
  1494. def _info_str(node):
  1495. # Returns information about the menu node 'node' as a string.
  1496. #
  1497. # The helper functions are responsible for adding newlines. This allows
  1498. # them to return "" if they don't want to add any output.
  1499. if isinstance(node.item, Symbol):
  1500. sym = node.item
  1501. return (
  1502. _name_info(sym) +
  1503. _help_info(sym) +
  1504. _direct_dep_info(sym) +
  1505. _defaults_info(sym) +
  1506. _select_imply_info(sym) +
  1507. _kconfig_def_info(sym)
  1508. )
  1509. if isinstance(node.item, Choice):
  1510. choice = node.item
  1511. return (
  1512. _name_info(choice) +
  1513. _help_info(choice) +
  1514. 'Mode: {}\n\n'.format(choice.str_value) +
  1515. _choice_syms_info(choice) +
  1516. _direct_dep_info(choice) +
  1517. _defaults_info(choice) +
  1518. _kconfig_def_info(choice)
  1519. )
  1520. # node.item in (MENU, COMMENT)
  1521. return _kconfig_def_info(node)
  1522. def _name_info(sc):
  1523. # Returns a string with the name of the symbol/choice. Choices are shown as
  1524. # <choice (name if any)>.
  1525. return (sc.name if sc.name else standard_sc_expr_str(sc)) + "\n\n"
  1526. def _value_info(sym):
  1527. # Returns a string showing 'sym's value
  1528. # Only put quotes around the value for string symbols
  1529. return "Value: {}\n".format(
  1530. '"{}"'.format(sym.str_value)
  1531. if sym.orig_type == STRING
  1532. else sym.str_value)
  1533. def _choice_syms_info(choice):
  1534. # Returns a string listing the choice symbols in 'choice'. Adds
  1535. # "(selected)" next to the selected one.
  1536. s = "Choice symbols:\n"
  1537. for sym in choice.syms:
  1538. s += " - " + sym.name
  1539. if sym is choice.selection:
  1540. s += " (selected)"
  1541. s += "\n"
  1542. return s + "\n"
  1543. def _help_info(sc):
  1544. # Returns a string with the help text(s) of 'sc' (Symbol or Choice).
  1545. # Symbols and choices defined in multiple locations can have multiple help
  1546. # texts.
  1547. s = ""
  1548. for node in sc.nodes:
  1549. if node.help is not None:
  1550. s += node.help + "\n\n"
  1551. return s
  1552. def _direct_dep_info(sc):
  1553. # Returns a string describing the direct dependencies of 'sc' (Symbol or
  1554. # Choice). The direct dependencies are the OR of the dependencies from each
  1555. # definition location. The dependencies at each definition location come
  1556. # from 'depends on' and dependencies inherited from parent items.
  1557. return "" if sc.direct_dep is _kconf.y else \
  1558. 'Direct dependencies (={}):\n{}\n' \
  1559. .format(TRI_TO_STR[expr_value(sc.direct_dep)],
  1560. _split_expr_info(sc.direct_dep, 2))
  1561. def _defaults_info(sc):
  1562. # Returns a string describing the defaults of 'sc' (Symbol or Choice)
  1563. if not sc.defaults:
  1564. return ""
  1565. s = "Defaults:\n"
  1566. for val, cond in sc.orig_defaults:
  1567. s += " - "
  1568. if isinstance(sc, Symbol):
  1569. s += _expr_str(val)
  1570. # Skip the tristate value hint if the expression is just a single
  1571. # symbol. _expr_str() already shows its value as a string.
  1572. #
  1573. # This also avoids showing the tristate value for string/int/hex
  1574. # defaults, which wouldn't make any sense.
  1575. if isinstance(val, tuple):
  1576. s += ' (={})'.format(TRI_TO_STR[expr_value(val)])
  1577. else:
  1578. # Don't print the value next to the symbol name for choice
  1579. # defaults, as it looks a bit confusing
  1580. s += val.name
  1581. s += "\n"
  1582. if cond is not _kconf.y:
  1583. s += " Condition (={}):\n{}" \
  1584. .format(TRI_TO_STR[expr_value(cond)],
  1585. _split_expr_info(cond, 4))
  1586. return s + "\n"
  1587. def _split_expr_info(expr, indent):
  1588. # Returns a string with 'expr' split into its top-level && or || operands,
  1589. # with one operand per line, together with the operand's value. This is
  1590. # usually enough to get something readable for long expressions. A fancier
  1591. # recursive thingy would be possible too.
  1592. #
  1593. # indent:
  1594. # Number of leading spaces to add before the split expression.
  1595. if len(split_expr(expr, AND)) > 1:
  1596. split_op = AND
  1597. op_str = "&&"
  1598. else:
  1599. split_op = OR
  1600. op_str = "||"
  1601. s = ""
  1602. for i, term in enumerate(split_expr(expr, split_op)):
  1603. s += "{}{} {}".format(indent*" ",
  1604. " " if i == 0 else op_str,
  1605. _expr_str(term))
  1606. # Don't bother showing the value hint if the expression is just a
  1607. # single symbol. _expr_str() already shows its value.
  1608. if isinstance(term, tuple):
  1609. s += " (={})".format(TRI_TO_STR[expr_value(term)])
  1610. s += "\n"
  1611. return s
  1612. def _select_imply_info(sym):
  1613. # Returns a string with information about which symbols 'select' or 'imply'
  1614. # 'sym'. The selecting/implying symbols are grouped according to which
  1615. # value they select/imply 'sym' to (n/m/y).
  1616. def sis(expr, val, title):
  1617. # sis = selects/implies
  1618. sis = [si for si in split_expr(expr, OR) if expr_value(si) == val]
  1619. if not sis:
  1620. return ""
  1621. res = title
  1622. for si in sis:
  1623. res += " - {}\n".format(split_expr(si, AND)[0].name)
  1624. return res + "\n"
  1625. s = ""
  1626. if sym.rev_dep is not _kconf.n:
  1627. s += sis(sym.rev_dep, 2,
  1628. "Symbols currently y-selecting this symbol:\n")
  1629. s += sis(sym.rev_dep, 1,
  1630. "Symbols currently m-selecting this symbol:\n")
  1631. s += sis(sym.rev_dep, 0,
  1632. "Symbols currently n-selecting this symbol (no effect):\n")
  1633. if sym.weak_rev_dep is not _kconf.n:
  1634. s += sis(sym.weak_rev_dep, 2,
  1635. "Symbols currently y-implying this symbol:\n")
  1636. s += sis(sym.weak_rev_dep, 1,
  1637. "Symbols currently m-implying this symbol:\n")
  1638. s += sis(sym.weak_rev_dep, 0,
  1639. "Symbols currently n-implying this symbol (no effect):\n")
  1640. return s
  1641. def _kconfig_def_info(item):
  1642. # Returns a string with the definition of 'item' in Kconfig syntax,
  1643. # together with the definition location(s) and their include and menu paths
  1644. nodes = [item] if isinstance(item, MenuNode) else item.nodes
  1645. s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \
  1646. .format("s" if len(nodes) > 1 else "")
  1647. s += (len(s) - 1)*"="
  1648. for node in nodes:
  1649. s += "\n\n" \
  1650. "At {}:{}\n" \
  1651. "{}" \
  1652. "Menu path: {}\n\n" \
  1653. "{}" \
  1654. .format(node.filename, node.linenr,
  1655. _include_path_info(node),
  1656. _menu_path_info(node),
  1657. node.custom_str(_name_and_val_str))
  1658. return s
  1659. def _include_path_info(node):
  1660. if not node.include_path:
  1661. # In the top-level Kconfig file
  1662. return ""
  1663. return "Included via {}\n".format(
  1664. " -> ".join("{}:{}".format(filename, linenr)
  1665. for filename, linenr in node.include_path))
  1666. def _menu_path_info(node):
  1667. # Returns a string describing the menu path leading up to 'node'
  1668. path = ""
  1669. while node.parent is not _kconf.top_node:
  1670. node = node.parent
  1671. # Promptless choices might appear among the parents. Use
  1672. # standard_sc_expr_str() for them, so that they show up as
  1673. # '<choice (name if any)>'.
  1674. path = " -> " + (node.prompt[0] if node.prompt else
  1675. standard_sc_expr_str(node.item)) + path
  1676. return "(Top)" + path
  1677. def _name_and_val_str(sc):
  1678. # Custom symbol/choice printer that shows symbol values after symbols
  1679. # Show the values of non-constant (non-quoted) symbols that don't look like
  1680. # numbers. Things like 123 are actually symbol references, and only work as
  1681. # expected due to undefined symbols getting their name as their value.
  1682. # Showing the symbol value for those isn't helpful though.
  1683. if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name):
  1684. if not sc.nodes:
  1685. # Undefined symbol reference
  1686. return "{}(undefined/n)".format(sc.name)
  1687. return '{}(={})'.format(sc.name, sc.str_value)
  1688. # For other items, use the standard format
  1689. return standard_sc_expr_str(sc)
  1690. def _expr_str(expr):
  1691. # Custom expression printer that shows symbol values
  1692. return expr_str(expr, _name_and_val_str)
  1693. def _is_num(name):
  1694. # Heuristic to see if a symbol name looks like a number, for nicer output
  1695. # when printing expressions. Things like 16 are actually symbol names, only
  1696. # they get their name as their value when the symbol is undefined.
  1697. try:
  1698. int(name)
  1699. except ValueError:
  1700. if not name.startswith(("0x", "0X")):
  1701. return False
  1702. try:
  1703. int(name, 16)
  1704. except ValueError:
  1705. return False
  1706. return True
  1707. if __name__ == "__main__":
  1708. _main()