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