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