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