1#!/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="word", 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()
2332