| #!/usr/bin/env python3 |
| |
| # Copyright (c) 2019, Nordic Semiconductor ASA and Ulf Magnusson |
| # SPDX-License-Identifier: ISC |
| |
| # _load_images() builds names dynamically to avoid having to give them twice |
| # (once for the variable and once for the filename). This forces consistency |
| # too. |
| # |
| # pylint: disable=undefined-variable |
| |
| """ |
| Overview |
| ======== |
| |
| A Tkinter-based menuconfig implementation, based around a treeview control and |
| a help display. The interface should feel familiar to people used to qconf |
| ('make xconfig'). Compatible with both Python 2 and Python 3. |
| |
| The display can be toggled between showing the full tree and showing just a |
| single menu (like menuconfig.py). Only single-menu mode distinguishes between |
| symbols defined with 'config' and symbols defined with 'menuconfig'. |
| |
| A show-all mode is available that shows invisible items in red. |
| |
| Supports both mouse and keyboard controls. The following keyboard shortcuts are |
| available: |
| |
| Ctrl-S : Save configuration |
| Ctrl-O : Open configuration |
| Ctrl-A : Toggle show-all mode |
| Ctrl-N : Toggle show-name mode |
| Ctrl-M : Toggle single-menu mode |
| Ctrl-F, /: Open jump-to dialog |
| ESC : Close |
| |
| Running |
| ======= |
| |
| guiconfig.py can be run either as a standalone executable or by calling the |
| menuconfig() function with an existing Kconfig instance. The second option is a |
| bit inflexible in that it will still load and save .config, etc. |
| |
| When run in standalone mode, the top-level Kconfig file to load can be passed |
| as a command-line argument. With no argument, it defaults to "Kconfig". |
| |
| The KCONFIG_CONFIG environment variable specifies the .config file to load (if |
| it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. |
| |
| When overwriting a configuration file, the old version is saved to |
| <filename>.old (e.g. .config.old). |
| |
| $srctree is supported through Kconfiglib. |
| """ |
| |
| # Note: There's some code duplication with menuconfig.py below, especially for |
| # the help text. Maybe some of it could be moved into kconfiglib.py or a shared |
| # helper script, but OTOH it's pretty nice to have things standalone and |
| # customizable. |
| |
| import errno |
| import os |
| import re |
| import sys |
| |
| _PY2 = sys.version_info[0] < 3 |
| |
| if _PY2: |
| # Python 2 |
| from Tkinter import * |
| import ttk |
| import tkFont as font |
| import tkFileDialog as filedialog |
| import tkMessageBox as messagebox |
| else: |
| # Python 3 |
| from tkinter import * |
| import tkinter.ttk as ttk |
| import tkinter.font as font |
| from tkinter import filedialog, messagebox |
| |
| from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ |
| BOOL, TRISTATE, STRING, INT, HEX, \ |
| AND, OR, \ |
| expr_str, expr_value, split_expr, \ |
| standard_sc_expr_str, \ |
| TRI_TO_STR, TYPE_TO_STR, \ |
| standard_kconfig, standard_config_filename |
| |
| |
| # If True, use GIF image data embedded in this file instead of separate GIF |
| # files. See _load_images(). |
| _USE_EMBEDDED_IMAGES = True |
| |
| |
| # Help text for the jump-to dialog |
| _JUMP_TO_HELP = """\ |
| Type one or more strings/regexes and press Enter to list items that match all |
| of them. Python's regex flavor is used (see the 're' module). Double-clicking |
| an item will jump to it. Item values can be toggled directly within the dialog.\ |
| """ |
| |
| |
| def _main(): |
| menuconfig(standard_kconfig(__doc__)) |
| |
| |
| # Global variables used below: |
| # |
| # _root: |
| # The Toplevel instance for the main window |
| # |
| # _tree: |
| # The Treeview in the main window |
| # |
| # _jump_to_tree: |
| # The Treeview in the jump-to dialog. None if the jump-to dialog isn't |
| # open. Doubles as a flag. |
| # |
| # _jump_to_matches: |
| # List of Nodes shown in the jump-to dialog |
| # |
| # _menupath: |
| # The Label that shows the menu path of the selected item |
| # |
| # _backbutton: |
| # The button shown in single-menu mode for jumping to the parent menu |
| # |
| # _status_label: |
| # Label with status text shown at the bottom of the main window |
| # ("Modified", "Saved to ...", etc.) |
| # |
| # _id_to_node: |
| # We can't use Node objects directly as Treeview item IDs, so we use their |
| # id()s instead. This dictionary maps Node id()s back to Nodes. (The keys |
| # are actually str(id(node)), just to simplify lookups.) |
| # |
| # _cur_menu: |
| # The current menu. Ignored outside single-menu mode. |
| # |
| # _show_all_var/_show_name_var/_single_menu_var: |
| # Tkinter Variable instances bound to the corresponding checkboxes |
| # |
| # _show_all/_single_menu: |
| # Plain Python bools that track _show_all_var and _single_menu_var, to |
| # speed up and simplify things a bit |
| # |
| # _conf_filename: |
| # File to save the configuration to |
| # |
| # _minconf_filename: |
| # File to save minimal configurations to |
| # |
| # _conf_changed: |
| # True if the configuration has been changed. If False, we don't bother |
| # showing the save-and-quit dialog. |
| # |
| # We reset this to False whenever the configuration is saved. |
| # |
| # _*_img: |
| # PhotoImage instances for images |
| |
| |
| def menuconfig(kconf): |
| """ |
| Launches the configuration interface, returning after the user exits. |
| |
| kconf: |
| Kconfig instance to be configured |
| """ |
| global _kconf |
| global _conf_filename |
| global _minconf_filename |
| global _jump_to_tree |
| global _cur_menu |
| |
| _kconf = kconf |
| |
| _jump_to_tree = None |
| |
| _create_id_to_node() |
| |
| _create_ui() |
| |
| # Filename to save configuration to |
| _conf_filename = standard_config_filename() |
| |
| # Load existing configuration and check if it's outdated |
| _set_conf_changed(_load_config()) |
| |
| # Filename to save minimal configuration to |
| _minconf_filename = "defconfig" |
| |
| # Current menu in single-menu mode |
| _cur_menu = _kconf.top_node |
| |
| # Any visible items in the top menu? |
| if not _shown_menu_nodes(kconf.top_node): |
| # Nothing visible. Start in show-all mode and try again. |
| _show_all_var.set(True) |
| if not _shown_menu_nodes(kconf.top_node): |
| # Give up and show an error. It's nice to be able to assume that |
| # the tree is non-empty in the rest of the code. |
| _root.wait_visibility() |
| messagebox.showerror( |
| "Error", |
| "Empty configuration -- nothing to configure.\n\n" |
| "Check that environment variables are set properly.") |
| _root.destroy() |
| return |
| |
| # Build the initial tree |
| _update_tree() |
| |
| # Select the first item and focus the Treeview, so that keyboard controls |
| # work immediately |
| _select(_tree, _tree.get_children()[0]) |
| _tree.focus_set() |
| |
| # Make geometry information available for centering the window. This |
| # indirectly creates the window, so hide it so that it's never shown at the |
| # old location. |
| _root.withdraw() |
| _root.update_idletasks() |
| |
| # Center the window |
| _root.geometry("+{}+{}".format( |
| (_root.winfo_screenwidth() - _root.winfo_reqwidth())//2, |
| (_root.winfo_screenheight() - _root.winfo_reqheight())//2)) |
| |
| # Show it |
| _root.deiconify() |
| |
| # Prevent the window from being automatically resized. Otherwise, it |
| # changes size when scrollbars appear/disappear before the user has |
| # manually resized it. |
| _root.geometry(_root.geometry()) |
| |
| _root.mainloop() |
| |
| |
| def _load_config(): |
| # Loads any existing .config file. See the Kconfig.load_config() docstring. |
| # |
| # Returns True if .config is missing or outdated. We always prompt for |
| # saving the configuration in that case. |
| |
| print(_kconf.load_config()) |
| if not os.path.exists(_conf_filename): |
| # No .config |
| return True |
| |
| return _needs_save() |
| |
| |
| def _needs_save(): |
| # Returns True if a just-loaded .config file is outdated (would get |
| # modified when saving) |
| |
| if _kconf.missing_syms: |
| # Assignments to undefined symbols in the .config |
| return True |
| |
| for sym in _kconf.unique_defined_syms: |
| if sym.user_value is None: |
| if sym.config_string: |
| # Unwritten symbol |
| return True |
| elif sym.orig_type in (BOOL, TRISTATE): |
| if sym.tri_value != sym.user_value: |
| # Written bool/tristate symbol, new value |
| return True |
| elif sym.str_value != sym.user_value: |
| # Written string/int/hex symbol, new value |
| return True |
| |
| # No need to prompt for save |
| return False |
| |
| |
| def _create_id_to_node(): |
| global _id_to_node |
| |
| _id_to_node = {str(id(node)): node for node in _kconf.node_iter()} |
| |
| |
| def _create_ui(): |
| # Creates the main window UI |
| |
| global _root |
| global _tree |
| |
| # Create the root window. This initializes Tkinter and makes e.g. |
| # PhotoImage available, so do it early. |
| _root = Tk() |
| |
| _load_images() |
| _init_misc_ui() |
| _fix_treeview_issues() |
| |
| _create_top_widgets() |
| # Create the pane with the Kconfig tree and description text |
| panedwindow, _tree = _create_kconfig_tree_and_desc(_root) |
| panedwindow.grid(column=0, row=1, sticky="nsew") |
| _create_status_bar() |
| |
| _root.columnconfigure(0, weight=1) |
| # Only the pane with the Kconfig tree and description grows vertically |
| _root.rowconfigure(1, weight=1) |
| |
| # Start with show-name disabled |
| _do_showname() |
| |
| _tree.bind("<Left>", _tree_left_key) |
| _tree.bind("<Right>", _tree_right_key) |
| # Note: Binding this for the jump-to tree as well would cause issues due to |
| # the Tk bug mentioned in _tree_open() |
| _tree.bind("<<TreeviewOpen>>", _tree_open) |
| # add=True to avoid overriding the description text update |
| _tree.bind("<<TreeviewSelect>>", _update_menu_path, add=True) |
| |
| _root.bind("<Control-s>", _save) |
| _root.bind("<Control-o>", _open) |
| _root.bind("<Control-a>", _toggle_showall) |
| _root.bind("<Control-n>", _toggle_showname) |
| _root.bind("<Control-m>", _toggle_tree_mode) |
| _root.bind("<Control-f>", _jump_to_dialog) |
| _root.bind("/", _jump_to_dialog) |
| _root.bind("<Escape>", _on_quit) |
| |
| |
| def _load_images(): |
| # Loads GIF images, creating the global _*_img PhotoImage variables. |
| # Base64-encoded images embedded in this script are used if |
| # _USE_EMBEDDED_IMAGES is True, and separate image files in the same |
| # directory as the script otherwise. |
| # |
| # Using a global variable indirectly prevents the image from being |
| # garbage-collected. Passing an image to a Tkinter function isn't enough to |
| # keep it alive. |
| |
| def load_image(name, data): |
| var_name = "_{}_img".format(name) |
| |
| if _USE_EMBEDDED_IMAGES: |
| globals()[var_name] = PhotoImage(data=data, format="gif") |
| else: |
| globals()[var_name] = PhotoImage( |
| file=os.path.join(os.path.dirname(__file__), name + ".gif"), |
| format="gif") |
| |
| # Note: Base64 data can be put on the clipboard with |
| # $ base64 -w0 foo.gif | xclip |
| |
| load_image("icon", "R0lGODlhMAAwAPEDAAAAAADQAO7u7v///yH5BAUKAAMALAAAAAAwADAAAAL/nI+gy+2Pokyv2jazuZxryQjiSJZmyXxHeLbumH6sEATvW8OLNtf5bfLZRLFITzgEipDJ4mYxYv6A0ubuqYhWk66tVTE4enHer7jcKvt0LLUw6P45lvEprT6c0+v7OBuqhYdHohcoqIbSAHc4ljhDwrh1UlgSydRCWWlp5wiYZvmSuSh4IzrqV6p4cwhkCsmY+nhK6uJ6t1mrOhuJqfu6+WYiCiwl7HtLjNSZZZis/MeM7NY3TaRKS40ooDeoiVqIultsrav92bi9c3a5KkkOsOJZpSS99m4k/0zPng4Gks9JSbB+8DIcoQfnjwpZCHv5W+ip4aQrKrB0uOikYhiMCBw1/uPoQUMBADs=") |
| load_image("n_bool", "R0lGODdhEAAQAPAAAAgICP///ywAAAAAEAAQAAACIISPacHtvp5kcb5qG85hZ2+BkyiRF8BBaEqtrKkqslEAADs=") |
| load_image("y_bool", "R0lGODdhEAAQAPEAAAgICADQAP///wAAACwAAAAAEAAQAAACMoSPacLtvlh4YrIYsst2cV19AvaVF9CUXBNJJoum7ymrsKuCnhiupIWjSSjAFuWhSCIKADs=") |
| load_image("n_tri", "R0lGODlhEAAQAPD/AAEBAf///yH5BAUKAAIALAAAAAAQABAAAAInlI+pBrAKQnCPSUlXvFhznlkfeGwjKZhnJ65h6nrfi6h0st2QXikFADs=") |
| load_image("m_tri", "R0lGODlhEAAQAPEDAAEBAeQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nI+pBrAWAhPCjYhiAJQCnWmdoElHGVBoiK5M21ofXFpXRIrgiecqxkuNciZIhNOZFRNI24PhfEoLADs=") |
| load_image("y_tri", "R0lGODlhEAAQAPEDAAICAgDQAP///wAAACH5BAUKAAMALAAAAAAQABAAAAI0nI+pBrAYBhDCRRUypfmergmgZ4xjMpmaw2zmxk7cCB+pWiVqp4MzDwn9FhGZ5WFjIZeGAgA7") |
| load_image("m_my", "R0lGODlhEAAQAPEDAAAAAOQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nIGpxiAPI2ghxFinq/ZygQhc94zgZopmOLYf67anGr+oZdp02emfV5n9MEHN5QhqICETxkABbQ4KADs=") |
| load_image("y_my", "R0lGODlhEAAQAPH/AAAAAADQAAPRA////yH5BAUKAAQALAAAAAAQABAAAAM+SArcrhCMSSuIM9Q8rxxBWIXawIBkmWonupLd565Um9G1PIs59fKmzw8WnAlusBYR2SEIN6DmAmqBLBxYSAIAOw==") |
| load_image("n_locked", "R0lGODlhEAAQAPABAAAAAP///yH5BAUKAAEALAAAAAAQABAAAAIgjB8AyKwN04pu0vMutpqqz4Hih4ydlnUpyl2r23pxUAAAOw==") |
| load_image("m_locked", "R0lGODlhEAAQAPD/AAAAAOQMuiH5BAUKAAIALAAAAAAQABAAAAIylC8AyKwN04ohnGcqqlZmfXDWI26iInZoyiore05walolV39ftxsYHgL9QBBMBGFEFAAAOw==") |
| load_image("y_locked", "R0lGODlhEAAQAPD/AAAAAADQACH5BAUKAAIALAAAAAAQABAAAAIylC8AyKzNgnlCtoDTwvZwrHydIYpQmR3KWq4uK74IOnp0HQPmnD3cOVlUIAgKsShkFAAAOw==") |
| load_image("not_selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIrlA2px6IBw2IpWglOvTYhzmUbGD3kNZ5QqrKn2YrqigCxZoMelU6No9gdCgA7") |
| load_image("selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIzlA2px6IBw2IpWglOvTah/kTZhimASJomiqonlLov1qptHTsgKSEzh9H8QI0QzNPwmRoFADs=") |
| load_image("edit", "R0lGODlhEAAQAPIFAAAAAKOLAMuuEPvXCvrxvgAAAAAAAAAAACH5BAUKAAUALAAAAAAQABAAAANCWLqw/gqMBp8cszJxcwVC2FEOEIAi5kVBi3IqWZhuCGMyfdpj2e4pnK+WAshmvxeAcETWlsxPkkBtsqBMa8TIBSQAADs=") |
| |
| |
| def _fix_treeview_issues(): |
| # Fixes some Treeview issues |
| |
| global _treeview_rowheight |
| |
| style = ttk.Style() |
| |
| # The treeview rowheight isn't adjusted automatically on high-DPI displays, |
| # so do it ourselves. The font will probably always be TkDefaultFont, but |
| # play it safe and look it up. |
| |
| _treeview_rowheight = font.Font(font=style.lookup("Treeview", "font")) \ |
| .metrics("linespace") + 2 |
| |
| style.configure("Treeview", rowheight=_treeview_rowheight) |
| |
| # Work around regression in https://core.tcl.tk/tk/tktview?name=509cafafae, |
| # which breaks tag background colors |
| |
| for option in "foreground", "background": |
| # Filter out any styles starting with ("!disabled", "!selected", ...). |
| # style.map() returns an empty list for missing options, so this should |
| # be future-safe. |
| style.map( |
| "Treeview", |
| **{option: [elm for elm in style.map("Treeview", query_opt=option) |
| if elm[:2] != ("!disabled", "!selected")]}) |
| |
| |
| def _init_misc_ui(): |
| # Does misc. UI initialization, like setting the title, icon, and theme |
| |
| _root.title(_kconf.mainmenu_text) |
| # iconphoto() isn't available in Python 2's Tkinter |
| _root.tk.call("wm", "iconphoto", _root._w, "-default", _icon_img) |
| # Reducing the width of the window to 1 pixel makes it move around, at |
| # least on GNOME. Prevent weird stuff like that. |
| _root.minsize(128, 128) |
| _root.protocol("WM_DELETE_WINDOW", _on_quit) |
| |
| # Use the 'clam' theme on *nix if it's available. It looks nicer than the |
| # 'default' theme. |
| if _root.tk.call("tk", "windowingsystem") == "x11": |
| style = ttk.Style() |
| if "clam" in style.theme_names(): |
| style.theme_use("clam") |
| |
| |
| def _create_top_widgets(): |
| # Creates the controls above the Kconfig tree in the main window |
| |
| global _show_all_var |
| global _show_name_var |
| global _single_menu_var |
| global _menupath |
| global _backbutton |
| |
| topframe = ttk.Frame(_root) |
| topframe.grid(column=0, row=0, sticky="ew") |
| |
| ttk.Button(topframe, text="Save", command=_save) \ |
| .grid(column=0, row=0, sticky="ew", padx=".05c", pady=".05c") |
| |
| ttk.Button(topframe, text="Save as...", command=_save_as) \ |
| .grid(column=1, row=0, sticky="ew") |
| |
| ttk.Button(topframe, text="Save minimal (advanced)...", |
| command=_save_minimal) \ |
| .grid(column=2, row=0, sticky="ew", padx=".05c") |
| |
| ttk.Button(topframe, text="Open...", command=_open) \ |
| .grid(column=3, row=0) |
| |
| ttk.Button(topframe, text="Jump to...", command=_jump_to_dialog) \ |
| .grid(column=4, row=0, padx=".05c") |
| |
| _show_name_var = BooleanVar() |
| ttk.Checkbutton(topframe, text="Show name", command=_do_showname, |
| variable=_show_name_var) \ |
| .grid(column=0, row=1, sticky="nsew", padx=".05c", pady="0 .05c", |
| ipady=".2c") |
| |
| _show_all_var = BooleanVar() |
| ttk.Checkbutton(topframe, text="Show all", command=_do_showall, |
| variable=_show_all_var) \ |
| .grid(column=1, row=1, sticky="nsew", pady="0 .05c") |
| |
| # Allow the show-all and single-menu status to be queried via plain global |
| # Python variables, which is faster and simpler |
| |
| def show_all_updated(*_): |
| global _show_all |
| _show_all = _show_all_var.get() |
| |
| _trace_write(_show_all_var, show_all_updated) |
| _show_all_var.set(False) |
| |
| _single_menu_var = BooleanVar() |
| ttk.Checkbutton(topframe, text="Single-menu mode", command=_do_tree_mode, |
| variable=_single_menu_var) \ |
| .grid(column=2, row=1, sticky="nsew", padx=".05c", pady="0 .05c") |
| |
| _backbutton = ttk.Button(topframe, text="<--", command=_leave_menu, |
| state="disabled") |
| _backbutton.grid(column=0, row=4, sticky="nsew", padx=".05c", pady="0 .05c") |
| |
| def tree_mode_updated(*_): |
| global _single_menu |
| _single_menu = _single_menu_var.get() |
| |
| if _single_menu: |
| _backbutton.grid() |
| else: |
| _backbutton.grid_remove() |
| |
| _trace_write(_single_menu_var, tree_mode_updated) |
| _single_menu_var.set(False) |
| |
| # Column to the right of the buttons that the menu path extends into, so |
| # that it can grow wider than the buttons |
| topframe.columnconfigure(5, weight=1) |
| |
| _menupath = ttk.Label(topframe) |
| _menupath.grid(column=0, row=3, columnspan=6, sticky="w", padx="0.05c", |
| pady="0 .05c") |
| |
| |
| def _create_kconfig_tree_and_desc(parent): |
| # Creates a Panedwindow with a Treeview that shows Kconfig nodes and a Text |
| # that shows a description of the selected node. Returns a tuple with the |
| # Panedwindow and the Treeview. This code is shared between the main window |
| # and the jump-to dialog. |
| |
| panedwindow = ttk.Panedwindow(parent, orient=VERTICAL) |
| |
| tree_frame, tree = _create_kconfig_tree(panedwindow) |
| desc_frame, desc = _create_kconfig_desc(panedwindow) |
| |
| panedwindow.add(tree_frame, weight=1) |
| panedwindow.add(desc_frame) |
| |
| def tree_select(_): |
| # The Text widget does not allow editing the text in its disabled |
| # state. We need to temporarily enable it. |
| desc["state"] = "normal" |
| |
| sel = tree.selection() |
| if not sel: |
| desc.delete("1.0", "end") |
| desc["state"] = "disabled" |
| return |
| |
| # Text.replace() is not available in Python 2's Tkinter |
| desc.delete("1.0", "end") |
| desc.insert("end", _info_str(_id_to_node[sel[0]])) |
| |
| desc["state"] = "disabled" |
| |
| tree.bind("<<TreeviewSelect>>", tree_select) |
| tree.bind("<1>", _tree_click) |
| tree.bind("<Double-1>", _tree_double_click) |
| tree.bind("<Return>", _tree_enter) |
| tree.bind("<KP_Enter>", _tree_enter) |
| tree.bind("<space>", _tree_toggle) |
| tree.bind("n", _tree_set_val(0)) |
| tree.bind("m", _tree_set_val(1)) |
| tree.bind("y", _tree_set_val(2)) |
| |
| return panedwindow, tree |
| |
| |
| def _create_kconfig_tree(parent): |
| # Creates a Treeview for showing Kconfig nodes |
| |
| frame = ttk.Frame(parent) |
| |
| tree = ttk.Treeview(frame, selectmode="browse", height=20, |
| columns=("name",)) |
| tree.heading("#0", text="Option", anchor="w") |
| tree.heading("name", text="Name", anchor="w") |
| |
| tree.tag_configure("n-bool", image=_n_bool_img) |
| tree.tag_configure("y-bool", image=_y_bool_img) |
| tree.tag_configure("m-tri", image=_m_tri_img) |
| tree.tag_configure("n-tri", image=_n_tri_img) |
| tree.tag_configure("m-tri", image=_m_tri_img) |
| tree.tag_configure("y-tri", image=_y_tri_img) |
| tree.tag_configure("m-my", image=_m_my_img) |
| tree.tag_configure("y-my", image=_y_my_img) |
| tree.tag_configure("n-locked", image=_n_locked_img) |
| tree.tag_configure("m-locked", image=_m_locked_img) |
| tree.tag_configure("y-locked", image=_y_locked_img) |
| tree.tag_configure("not-selected", image=_not_selected_img) |
| tree.tag_configure("selected", image=_selected_img) |
| tree.tag_configure("edit", image=_edit_img) |
| tree.tag_configure("invisible", foreground="red") |
| |
| tree.grid(column=0, row=0, sticky="nsew") |
| |
| _add_vscrollbar(frame, tree) |
| |
| frame.columnconfigure(0, weight=1) |
| frame.rowconfigure(0, weight=1) |
| |
| # Create items for all menu nodes. These can be detached/moved later. |
| # Micro-optimize this a bit. |
| insert = tree.insert |
| id_ = id |
| Symbol_ = Symbol |
| for node in _kconf.node_iter(): |
| item = node.item |
| insert("", "end", iid=id_(node), |
| values=item.name if item.__class__ is Symbol_ else "") |
| |
| return frame, tree |
| |
| |
| def _create_kconfig_desc(parent): |
| # Creates a Text for showing the description of the selected Kconfig node |
| |
| frame = ttk.Frame(parent) |
| |
| desc = Text(frame, height=12, wrap="word", borderwidth=0, |
| state="disabled") |
| desc.grid(column=0, row=0, sticky="nsew") |
| |
| # Work around not being to Ctrl-C/V text from a disabled Text widget, with a |
| # tip found in https://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only |
| desc.bind("<1>", lambda _: desc.focus_set()) |
| |
| _add_vscrollbar(frame, desc) |
| |
| frame.columnconfigure(0, weight=1) |
| frame.rowconfigure(0, weight=1) |
| |
| return frame, desc |
| |
| |
| def _add_vscrollbar(parent, widget): |
| # Adds a vertical scrollbar to 'widget' that's only shown as needed |
| |
| vscrollbar = ttk.Scrollbar(parent, orient="vertical", |
| command=widget.yview) |
| vscrollbar.grid(column=1, row=0, sticky="ns") |
| |
| def yscrollcommand(first, last): |
| # Only show the scrollbar when needed. 'first' and 'last' are |
| # strings. |
| if float(first) <= 0.0 and float(last) >= 1.0: |
| vscrollbar.grid_remove() |
| else: |
| vscrollbar.grid() |
| |
| vscrollbar.set(first, last) |
| |
| widget["yscrollcommand"] = yscrollcommand |
| |
| |
| def _create_status_bar(): |
| # Creates the status bar at the bottom of the main window |
| |
| global _status_label |
| |
| _status_label = ttk.Label(_root, anchor="e", padding="0 0 0.4c 0") |
| _status_label.grid(column=0, row=3, sticky="ew") |
| |
| |
| def _set_status(s): |
| # Sets the text in the status bar to 's' |
| |
| _status_label["text"] = s |
| |
| |
| def _set_conf_changed(changed): |
| # Updates the status re. whether there are unsaved changes |
| |
| global _conf_changed |
| |
| _conf_changed = changed |
| if changed: |
| _set_status("Modified") |
| |
| |
| def _update_tree(): |
| # Updates the Kconfig tree in the main window by first detaching all nodes |
| # and then updating and reattaching them. The tree structure might have |
| # changed. |
| |
| # If a selected/focused item is detached and later reattached, it stays |
| # selected/focused. That can give multiple selections even though |
| # selectmode=browse. Save and later restore the selection and focus as a |
| # workaround. |
| old_selection = _tree.selection() |
| old_focus = _tree.focus() |
| |
| # Detach all tree items before re-stringing them. This is relatively fast, |
| # luckily. |
| _tree.detach(*_id_to_node.keys()) |
| |
| if _single_menu: |
| _build_menu_tree() |
| else: |
| _build_full_tree(_kconf.top_node) |
| |
| _tree.selection_set(old_selection) |
| _tree.focus(old_focus) |
| |
| |
| def _build_full_tree(menu): |
| # Updates the tree starting from menu.list, in full-tree mode. To speed |
| # things up, only open menus are updated. The menu-at-a-time logic here is |
| # to deal with invisible items that can show up outside show-all mode (see |
| # _shown_full_nodes()). |
| |
| for node in _shown_full_nodes(menu): |
| _add_to_tree(node, _kconf.top_node) |
| |
| # _shown_full_nodes() includes nodes from menus rooted at symbols, so |
| # we only need to check "real" menus/choices here |
| if node.list and not isinstance(node.item, Symbol): |
| if _tree.item(id(node), "open"): |
| _build_full_tree(node) |
| else: |
| # We're just probing here, so _shown_menu_nodes() will work |
| # fine, and might be a bit faster |
| shown = _shown_menu_nodes(node) |
| if shown: |
| # Dummy element to make the open/closed toggle appear |
| _tree.move(id(shown[0]), id(shown[0].parent), "end") |
| |
| |
| def _shown_full_nodes(menu): |
| # Returns the list of menu nodes shown in 'menu' (a menu node for a menu) |
| # for full-tree mode. A tricky detail is that invisible items need to be |
| # shown if they have visible children. |
| |
| def rec(node): |
| res = [] |
| |
| while node: |
| if _visible(node) or _show_all: |
| res.append(node) |
| if node.list and isinstance(node.item, Symbol): |
| # Nodes from menu created from dependencies |
| res += rec(node.list) |
| |
| elif node.list and isinstance(node.item, Symbol): |
| # Show invisible symbols (defined with either 'config' and |
| # 'menuconfig') if they have visible children. This can happen |
| # for an m/y-valued symbol with an optional prompt |
| # ('prompt "foo" is COND') that is currently disabled. |
| shown_children = rec(node.list) |
| if shown_children: |
| res.append(node) |
| res += shown_children |
| |
| node = node.next |
| |
| return res |
| |
| return rec(menu.list) |
| |
| |
| def _build_menu_tree(): |
| # Updates the tree in single-menu mode. See _build_full_tree() as well. |
| |
| for node in _shown_menu_nodes(_cur_menu): |
| _add_to_tree(node, _cur_menu) |
| |
| |
| def _shown_menu_nodes(menu): |
| # Used for single-menu mode. Similar to _shown_full_nodes(), but doesn't |
| # include children of symbols defined with 'menuconfig'. |
| |
| def rec(node): |
| res = [] |
| |
| while node: |
| if _visible(node) or _show_all: |
| res.append(node) |
| if node.list and not node.is_menuconfig: |
| res += rec(node.list) |
| |
| elif node.list and isinstance(node.item, Symbol): |
| shown_children = rec(node.list) |
| if shown_children: |
| # Invisible item with visible children |
| res.append(node) |
| if not node.is_menuconfig: |
| res += shown_children |
| |
| node = node.next |
| |
| return res |
| |
| return rec(menu.list) |
| |
| |
| def _visible(node): |
| # Returns True if the node should appear in the menu (outside show-all |
| # mode) |
| |
| return node.prompt and expr_value(node.prompt[1]) and not \ |
| (node.item == MENU and not expr_value(node.visibility)) |
| |
| |
| def _add_to_tree(node, top): |
| # Adds 'node' to the tree, at the end of its menu. We rely on going through |
| # the nodes linearly to get the correct order. 'top' holds the menu that |
| # corresponds to the top-level menu, and can vary in single-menu mode. |
| |
| parent = node.parent |
| _tree.move(id(node), "" if parent is top else id(parent), "end") |
| _tree.item( |
| id(node), |
| text=_node_str(node), |
| # The _show_all test avoids showing invisible items in red outside |
| # show-all mode, which could look confusing/broken. Invisible symbols |
| # are shown outside show-all mode if an invisible symbol has visible |
| # children in an implicit menu. |
| tags=_img_tag(node) if _visible(node) or not _show_all else |
| _img_tag(node) + " invisible") |
| |
| |
| def _node_str(node): |
| # Returns the string shown to the right of the image (if any) for the node |
| |
| if node.prompt: |
| if node.item == COMMENT: |
| s = "*** {} ***".format(node.prompt[0]) |
| else: |
| s = node.prompt[0] |
| |
| if isinstance(node.item, Symbol): |
| sym = node.item |
| |
| # Print "(NEW)" next to symbols without a user value (from e.g. a |
| # .config), but skip it for choice symbols in choices in y mode, |
| # and for symbols of UNKNOWN type (which generate a warning though) |
| if sym.user_value is None and sym.type and not \ |
| (sym.choice and sym.choice.tri_value == 2): |
| |
| s += " (NEW)" |
| |
| elif isinstance(node.item, Symbol): |
| # Symbol without prompt (can show up in show-all) |
| s = "<{}>".format(node.item.name) |
| |
| else: |
| # Choice without prompt. Use standard_sc_expr_str() so that it shows up |
| # as '<choice (name if any)>'. |
| s = standard_sc_expr_str(node.item) |
| |
| |
| if isinstance(node.item, Symbol): |
| sym = node.item |
| if sym.orig_type == STRING: |
| s += ": " + sym.str_value |
| elif sym.orig_type in (INT, HEX): |
| s = "({}) {}".format(sym.str_value, s) |
| |
| elif isinstance(node.item, Choice) and node.item.tri_value == 2: |
| # Print the prompt of the selected symbol after the choice for |
| # choices in y mode |
| sym = node.item.selection |
| if sym: |
| for sym_node in sym.nodes: |
| # Use the prompt used at this choice location, in case the |
| # choice symbol is defined in multiple locations |
| if sym_node.parent is node and sym_node.prompt: |
| s += " ({})".format(sym_node.prompt[0]) |
| break |
| else: |
| # If the symbol isn't defined at this choice location, then |
| # just use whatever prompt we can find for it |
| for sym_node in sym.nodes: |
| if sym_node.prompt: |
| s += " ({})".format(sym_node.prompt[0]) |
| break |
| |
| # In single-menu mode, print "--->" next to nodes that have menus that can |
| # potentially be entered. Print "----" if the menu is empty. We don't allow |
| # those to be entered. |
| if _single_menu and node.is_menuconfig: |
| s += " --->" if _shown_menu_nodes(node) else " ----" |
| |
| return s |
| |
| |
| def _img_tag(node): |
| # Returns the tag for the image that should be shown next to 'node', or the |
| # empty string if it shouldn't have an image |
| |
| item = node.item |
| |
| if item in (MENU, COMMENT) or not item.orig_type: |
| return "" |
| |
| if item.orig_type in (STRING, INT, HEX): |
| return "edit" |
| |
| # BOOL or TRISTATE |
| |
| if _is_y_mode_choice_sym(item): |
| # Choice symbol in y-mode choice |
| return "selected" if item.choice.selection is item else "not-selected" |
| |
| if len(item.assignable) <= 1: |
| # Pinned to a single value |
| return "" if isinstance(item, Choice) else item.str_value + "-locked" |
| |
| if item.type == BOOL: |
| return item.str_value + "-bool" |
| |
| # item.type == TRISTATE |
| if item.assignable == (1, 2): |
| return item.str_value + "-my" |
| return item.str_value + "-tri" |
| |
| |
| def _is_y_mode_choice_sym(item): |
| # The choice mode is an upper bound on the visibility of choice symbols, so |
| # we can check the choice symbols' own visibility to see if the choice is |
| # in y mode |
| return isinstance(item, Symbol) and item.choice and item.visibility == 2 |
| |
| |
| def _tree_click(event): |
| # Click on the Kconfig Treeview |
| |
| tree = event.widget |
| if tree.identify_element(event.x, event.y) == "image": |
| item = tree.identify_row(event.y) |
| # Select the item before possibly popping up a dialog for |
| # string/int/hex items, so that its help is visible |
| _select(tree, item) |
| _change_node(_id_to_node[item], tree.winfo_toplevel()) |
| return "break" |
| |
| |
| def _tree_double_click(event): |
| # Double-click on the Kconfig treeview |
| |
| # Do an extra check to avoid weirdness when double-clicking in the tree |
| # heading area |
| if not _in_heading(event): |
| return _tree_enter(event) |
| |
| |
| def _in_heading(event): |
| # Returns True if 'event' took place in the tree heading |
| |
| tree = event.widget |
| return hasattr(tree, "identify_region") and \ |
| tree.identify_region(event.x, event.y) in ("heading", "separator") |
| |
| |
| def _tree_enter(event): |
| # Enter press or double-click within the Kconfig treeview. Prefer to |
| # open/close/enter menus, but toggle the value if that's not possible. |
| |
| tree = event.widget |
| sel = tree.focus() |
| if sel: |
| node = _id_to_node[sel] |
| |
| if tree.get_children(sel): |
| _tree_toggle_open(sel) |
| elif _single_menu_mode_menu(node, tree): |
| _enter_menu_and_select_first(node) |
| else: |
| _change_node(node, tree.winfo_toplevel()) |
| |
| return "break" |
| |
| |
| def _tree_toggle(event): |
| # Space press within the Kconfig treeview. Prefer to toggle the value, but |
| # open/close/enter the menu if that's not possible. |
| |
| tree = event.widget |
| sel = tree.focus() |
| if sel: |
| node = _id_to_node[sel] |
| |
| if _changeable(node): |
| _change_node(node, tree.winfo_toplevel()) |
| elif _single_menu_mode_menu(node, tree): |
| _enter_menu_and_select_first(node) |
| elif tree.get_children(sel): |
| _tree_toggle_open(sel) |
| |
| return "break" |
| |
| |
| def _tree_left_key(_): |
| # Left arrow key press within the Kconfig treeview |
| |
| if _single_menu: |
| # Leave the current menu in single-menu mode |
| _leave_menu() |
| return "break" |
| |
| # Otherwise, default action |
| |
| |
| def _tree_right_key(_): |
| # Right arrow key press within the Kconfig treeview |
| |
| sel = _tree.focus() |
| if sel: |
| node = _id_to_node[sel] |
| # If the node can be entered in single-menu mode, do it |
| if _single_menu_mode_menu(node, _tree): |
| _enter_menu_and_select_first(node) |
| return "break" |
| |
| # Otherwise, default action |
| |
| |
| def _single_menu_mode_menu(node, tree): |
| # Returns True if single-menu mode is on and 'node' is an (interface) |
| # menu that can be entered |
| |
| return _single_menu and tree is _tree and node.is_menuconfig and \ |
| _shown_menu_nodes(node) |
| |
| |
| def _changeable(node): |
| # Returns True if 'node' is a Symbol/Choice whose value can be changed |
| |
| sc = node.item |
| |
| if not isinstance(sc, (Symbol, Choice)): |
| return False |
| |
| # This will hit for invisible symbols, which appear in show-all mode and |
| # when an invisible symbol has visible children (which can happen e.g. for |
| # symbols with optional prompts) |
| if not (node.prompt and expr_value(node.prompt[1])): |
| return False |
| |
| return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ |
| or _is_y_mode_choice_sym(sc) |
| |
| |
| def _tree_toggle_open(item): |
| # Opens/closes the Treeview item 'item' |
| |
| if _tree.item(item, "open"): |
| _tree.item(item, open=False) |
| else: |
| node = _id_to_node[item] |
| if not isinstance(node.item, Symbol): |
| # Can only get here in full-tree mode |
| _build_full_tree(node) |
| _tree.item(item, open=True) |
| |
| |
| def _tree_set_val(tri_val): |
| def tree_set_val(event): |
| # n/m/y press within the Kconfig treeview |
| |
| # Sets the value of the currently selected item to 'tri_val', if that |
| # value can be assigned |
| |
| sel = event.widget.focus() |
| if sel: |
| sc = _id_to_node[sel].item |
| if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: |
| _set_val(sc, tri_val) |
| |
| return tree_set_val |
| |
| |
| def _tree_open(_): |
| # Lazily populates the Kconfig tree when menus are opened in full-tree mode |
| |
| if _single_menu: |
| # Work around https://core.tcl.tk/tk/tktview?name=368fa4561e |
| # ("ttk::treeview open/closed indicators can be toggled while hidden"). |
| # Clicking on the hidden indicator will call _build_full_tree() in |
| # single-menu mode otherwise. |
| return |
| |
| node = _id_to_node[_tree.focus()] |
| # _shown_full_nodes() includes nodes from menus rooted at symbols, so we |
| # only need to check "real" menus and choices here |
| if not isinstance(node.item, Symbol): |
| _build_full_tree(node) |
| |
| |
| def _update_menu_path(_): |
| # Updates the displayed menu path when nodes are selected in the Kconfig |
| # treeview |
| |
| sel = _tree.selection() |
| _menupath["text"] = _menu_path_info(_id_to_node[sel[0]]) if sel else "" |
| |
| |
| def _item_row(item): |
| # Returns the row number 'item' appears on within the Kconfig treeview, |
| # starting from the top of the tree. Used to preserve scrolling. |
| # |
| # ttkTreeview.c in the Tk sources defines a RowNumber() function that does |
| # the same thing, but it's not exposed. |
| |
| row = 0 |
| |
| while True: |
| prev = _tree.prev(item) |
| if prev: |
| item = prev |
| row += _n_rows(item) |
| else: |
| item = _tree.parent(item) |
| if not item: |
| return row |
| row += 1 |
| |
| |
| def _n_rows(item): |
| # _item_row() helper. Returns the number of rows occupied by 'item' and # |
| # its children. |
| |
| rows = 1 |
| |
| if _tree.item(item, "open"): |
| for child in _tree.get_children(item): |
| rows += _n_rows(child) |
| |
| return rows |
| |
| |
| def _attached(item): |
| # Heuristic for checking if a Treeview item is attached. Doesn't seem to be |
| # good APIs for this. Might fail for super-obscure cases with tiny trees, |
| # but you'd just get a small scroll mess-up. |
| |
| return bool(_tree.next(item) or _tree.prev(item) or _tree.parent(item)) |
| |
| |
| def _change_node(node, parent): |
| # Toggles/changes the value of 'node'. 'parent' is the parent window |
| # (either the main window or the jump-to dialog), in case we need to pop up |
| # a dialog. |
| |
| if not _changeable(node): |
| return |
| |
| # sc = symbol/choice |
| sc = node.item |
| |
| if sc.type in (INT, HEX, STRING): |
| s = _set_val_dialog(node, parent) |
| |
| # Tkinter can return 'unicode' strings on Python 2, which Kconfiglib |
| # can't deal with. UTF-8-encode the string to work around it. |
| if _PY2 and isinstance(s, unicode): |
| s = s.encode("utf-8", "ignore") |
| |
| if s is not None: |
| _set_val(sc, s) |
| |
| elif len(sc.assignable) == 1: |
| # Handles choice symbols for choices in y mode, which are a special |
| # case: .assignable can be (2,) while .tri_value is 0. |
| _set_val(sc, sc.assignable[0]) |
| |
| else: |
| # Set the symbol to the value after the current value in |
| # sc.assignable, with wrapping |
| val_index = sc.assignable.index(sc.tri_value) |
| _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) |
| |
| |
| def _set_val(sc, val): |
| # Wrapper around Symbol/Choice.set_value() for updating the menu state and |
| # _conf_changed |
| |
| # Use the string representation of tristate values. This makes the format |
| # consistent for all symbol types. |
| if val in TRI_TO_STR: |
| val = TRI_TO_STR[val] |
| |
| if val != sc.str_value: |
| sc.set_value(val) |
| _set_conf_changed(True) |
| |
| # Update the tree and try to preserve the scroll. Do a cheaper variant |
| # than in the show-all case, that might mess up the scroll slightly in |
| # rare cases, but is fast and flicker-free. |
| |
| stayput = _loc_ref_item() # Item to preserve scroll for |
| old_row = _item_row(stayput) |
| |
| _update_tree() |
| |
| # If the reference item disappeared (can happen if the change was done |
| # from the jump-to dialog), then avoid messing with the scroll and hope |
| # for the best |
| if _attached(stayput): |
| _tree.yview_scroll(_item_row(stayput) - old_row, "units") |
| |
| if _jump_to_tree: |
| _update_jump_to_display() |
| |
| |
| def _set_val_dialog(node, parent): |
| # Pops up a dialog for setting the value of the string/int/hex |
| # symbol at node 'node'. 'parent' is the parent window. |
| |
| def ok(_=None): |
| # No 'nonlocal' in Python 2 |
| global _entry_res |
| |
| s = entry.get() |
| if sym.type == HEX and not s.startswith(("0x", "0X")): |
| s = "0x" + s |
| |
| if _check_valid(dialog, entry, sym, s): |
| _entry_res = s |
| dialog.destroy() |
| |
| def cancel(_=None): |
| global _entry_res |
| _entry_res = None |
| dialog.destroy() |
| |
| sym = node.item |
| |
| dialog = Toplevel(parent) |
| dialog.title("Enter {} value".format(TYPE_TO_STR[sym.type])) |
| dialog.resizable(False, False) |
| dialog.transient(parent) |
| dialog.protocol("WM_DELETE_WINDOW", cancel) |
| |
| ttk.Label(dialog, text=node.prompt[0] + ":") \ |
| .grid(column=0, row=0, columnspan=2, sticky="w", padx=".3c", |
| pady=".2c .05c") |
| |
| entry = ttk.Entry(dialog, width=30) |
| # Start with the previous value in the editbox, selected |
| entry.insert(0, sym.str_value) |
| entry.selection_range(0, "end") |
| entry.grid(column=0, row=1, columnspan=2, sticky="ew", padx=".3c") |
| entry.focus_set() |
| |
| range_info = _range_info(sym) |
| if range_info: |
| ttk.Label(dialog, text=range_info) \ |
| .grid(column=0, row=2, columnspan=2, sticky="w", padx=".3c", |
| pady=".2c 0") |
| |
| ttk.Button(dialog, text="OK", command=ok) \ |
| .grid(column=0, row=4 if range_info else 3, sticky="e", padx=".3c", |
| pady=".4c") |
| |
| ttk.Button(dialog, text="Cancel", command=cancel) \ |
| .grid(column=1, row=4 if range_info else 3, padx="0 .3c") |
| |
| # Give all horizontal space to the grid cell with the OK button, so that |
| # Cancel moves to the right |
| dialog.columnconfigure(0, weight=1) |
| |
| _center_on_root(dialog) |
| |
| # Hack to scroll the entry so that the end of the text is shown, from |
| # https://stackoverflow.com/questions/29334544/why-does-tkinters-entry-xview-moveto-fail. |
| # Related Tk ticket: https://core.tcl.tk/tk/info/2513186fff |
| def scroll_entry(_): |
| _root.update_idletasks() |
| entry.unbind("<Expose>") |
| entry.xview_moveto(1) |
| entry.bind("<Expose>", scroll_entry) |
| |
| # The dialog must be visible before we can grab the input |
| dialog.wait_visibility() |
| dialog.grab_set() |
| |
| dialog.bind("<Return>", ok) |
| dialog.bind("<KP_Enter>", ok) |
| dialog.bind("<Escape>", cancel) |
| |
| # Wait for the user to be done with the dialog |
| parent.wait_window(dialog) |
| |
| # Regrab the input in the parent |
| parent.grab_set() |
| |
| return _entry_res |
| |
| |
| def _center_on_root(dialog): |
| # Centers 'dialog' on the root window. It often ends up at some bad place |
| # like the top-left corner of the screen otherwise. See the menuconfig() |
| # function, which has similar logic. |
| |
| dialog.withdraw() |
| _root.update_idletasks() |
| |
| dialog_width = dialog.winfo_reqwidth() |
| dialog_height = dialog.winfo_reqheight() |
| |
| screen_width = _root.winfo_screenwidth() |
| screen_height = _root.winfo_screenheight() |
| |
| x = _root.winfo_rootx() + (_root.winfo_width() - dialog_width)//2 |
| y = _root.winfo_rooty() + (_root.winfo_height() - dialog_height)//2 |
| |
| # Clamp so that no part of the dialog is outside the screen |
| if x + dialog_width > screen_width: |
| x = screen_width - dialog_width |
| elif x < 0: |
| x = 0 |
| if y + dialog_height > screen_height: |
| y = screen_height - dialog_height |
| elif y < 0: |
| y = 0 |
| |
| dialog.geometry("+{}+{}".format(x, y)) |
| |
| dialog.deiconify() |
| |
| |
| def _check_valid(dialog, entry, sym, s): |
| # Returns True if the string 's' is a well-formed value for 'sym'. |
| # Otherwise, pops up an error and returns False. |
| |
| if sym.type not in (INT, HEX): |
| # Anything goes for non-int/hex symbols |
| return True |
| |
| base = 10 if sym.type == INT else 16 |
| try: |
| int(s, base) |
| except ValueError: |
| messagebox.showerror( |
| "Bad value", |
| "'{}' is a malformed {} value".format( |
| s, TYPE_TO_STR[sym.type]), |
| parent=dialog) |
| entry.focus_set() |
| return False |
| |
| for low_sym, high_sym, cond in sym.ranges: |
| if expr_value(cond): |
| low_s = low_sym.str_value |
| high_s = high_sym.str_value |
| |
| if not int(low_s, base) <= int(s, base) <= int(high_s, base): |
| messagebox.showerror( |
| "Value out of range", |
| "{} is outside the range {}-{}".format(s, low_s, high_s), |
| parent=dialog) |
| entry.focus_set() |
| return False |
| |
| break |
| |
| return True |
| |
| |
| def _range_info(sym): |
| # Returns a string with information about the valid range for the symbol |
| # 'sym', or None if 'sym' doesn't have a range |
| |
| if sym.type in (INT, HEX): |
| for low, high, cond in sym.ranges: |
| if expr_value(cond): |
| return "Range: {}-{}".format(low.str_value, high.str_value) |
| |
| return None |
| |
| |
| def _save(_=None): |
| # Tries to save the configuration |
| |
| if _try_save(_kconf.write_config, _conf_filename, "configuration"): |
| _set_conf_changed(False) |
| |
| _tree.focus_set() |
| |
| |
| def _save_as(): |
| # Pops up a dialog for saving the configuration to a specific location |
| |
| global _conf_filename |
| |
| filename = _conf_filename |
| while True: |
| filename = filedialog.asksaveasfilename( |
| title="Save configuration as", |
| initialdir=os.path.dirname(filename), |
| initialfile=os.path.basename(filename), |
| parent=_root) |
| |
| if not filename: |
| break |
| |
| if _try_save(_kconf.write_config, filename, "configuration"): |
| _conf_filename = filename |
| break |
| |
| _tree.focus_set() |
| |
| |
| def _save_minimal(): |
| # Pops up a dialog for saving a minimal configuration (defconfig) to a |
| # specific location |
| |
| global _minconf_filename |
| |
| filename = _minconf_filename |
| while True: |
| filename = filedialog.asksaveasfilename( |
| title="Save minimal configuration as", |
| initialdir=os.path.dirname(filename), |
| initialfile=os.path.basename(filename), |
| parent=_root) |
| |
| if not filename: |
| break |
| |
| if _try_save(_kconf.write_min_config, filename, |
| "minimal configuration"): |
| |
| _minconf_filename = filename |
| break |
| |
| _tree.focus_set() |
| |
| |
| def _open(_=None): |
| # Pops up a dialog for loading a configuration |
| |
| global _conf_filename |
| |
| if _conf_changed and \ |
| not messagebox.askokcancel( |
| "Unsaved changes", |
| "You have unsaved changes. Load new configuration anyway?"): |
| |
| return |
| |
| filename = _conf_filename |
| while True: |
| filename = filedialog.askopenfilename( |
| title="Open configuration", |
| initialdir=os.path.dirname(filename), |
| initialfile=os.path.basename(filename), |
| parent=_root) |
| |
| if not filename: |
| break |
| |
| if _try_load(filename): |
| # Maybe something fancier could be done here later to try to |
| # preserve the scroll |
| |
| _conf_filename = filename |
| _set_conf_changed(_needs_save()) |
| |
| if _single_menu and not _shown_menu_nodes(_cur_menu): |
| # Turn on show-all if we're in single-menu mode and would end |
| # up with an empty menu |
| _show_all_var.set(True) |
| |
| _update_tree() |
| |
| break |
| |
| _tree.focus_set() |
| |
| |
| def _toggle_showname(_): |
| # Toggles show-name mode on/off |
| |
| _show_name_var.set(not _show_name_var.get()) |
| _do_showname() |
| |
| |
| def _do_showname(): |
| # Updates the UI for the current show-name setting |
| |
| # Columns do not automatically shrink/expand, so we have to update |
| # column widths ourselves |
| |
| tree_width = _tree.winfo_width() |
| |
| if _show_name_var.get(): |
| _tree["displaycolumns"] = ("name",) |
| _tree["show"] = "tree headings" |
| name_width = tree_width//3 |
| _tree.column("#0", width=max(tree_width - name_width, 1)) |
| _tree.column("name", width=name_width) |
| else: |
| _tree["displaycolumns"] = () |
| _tree["show"] = "tree" |
| _tree.column("#0", width=tree_width) |
| |
| _tree.focus_set() |
| |
| |
| def _toggle_showall(_): |
| # Toggles show-all mode on/off |
| |
| _show_all_var.set(not _show_all) |
| _do_showall() |
| |
| |
| def _do_showall(): |
| # Updates the UI for the current show-all setting |
| |
| # Don't allow turning off show-all if we'd end up with no visible nodes |
| if _nothing_shown(): |
| _show_all_var.set(True) |
| return |
| |
| # Save scroll information. old_scroll can end up negative here, if the |
| # reference item isn't shown (only invisible items on the screen, and |
| # show-all being turned off). |
| |
| stayput = _vis_loc_ref_item() |
| # Probe the middle of the first row, to play it safe. identify_row(0) seems |
| # to return the row before the top row. |
| old_scroll = _item_row(stayput) - \ |
| _item_row(_tree.identify_row(_treeview_rowheight//2)) |
| |
| _update_tree() |
| |
| if _show_all: |
| # Deep magic: Unless we call update_idletasks(), the scroll adjustment |
| # below is restricted to the height of the old tree, instead of the |
| # height of the new tree. Since the tree with show-all on is guaranteed |
| # to be taller, and we want the maximum range, we only call it when |
| # turning show-all on. |
| # |
| # Strictly speaking, something similar ought to be done when changing |
| # symbol values, but it causes annoying flicker, and in 99% of cases |
| # things work anyway there (with usually minor scroll mess-ups in the |
| # 1% case). |
| _root.update_idletasks() |
| |
| # Restore scroll |
| _tree.yview(_item_row(stayput) - old_scroll) |
| |
| _tree.focus_set() |
| |
| |
| def _nothing_shown(): |
| # _do_showall() helper. Returns True if no nodes would get |
| # shown with the current show-all setting. Also handles the |
| # (obscure) case when there are no visible nodes in the entire |
| # tree, meaning guiconfig was automatically started in |
| # show-all mode, which mustn't be turned off. |
| |
| return not _shown_menu_nodes( |
| _cur_menu if _single_menu else _kconf.top_node) |
| |
| |
| def _toggle_tree_mode(_): |
| # Toggles single-menu mode on/off |
| |
| _single_menu_var.set(not _single_menu) |
| _do_tree_mode() |
| |
| |
| def _do_tree_mode(): |
| # Updates the UI for the current tree mode (full-tree or single-menu) |
| |
| loc_ref_node = _id_to_node[_loc_ref_item()] |
| |
| if not _single_menu: |
| # _jump_to() -> _enter_menu() already updates the tree, but |
| # _jump_to() -> load_parents() doesn't, because it isn't always needed. |
| # We always need to update the tree here, e.g. to add/remove "--->". |
| _update_tree() |
| |
| _jump_to(loc_ref_node) |
| _tree.focus_set() |
| |
| |
| def _enter_menu_and_select_first(menu): |
| # Enters the menu 'menu' and selects the first item. Used in single-menu |
| # mode. |
| |
| _enter_menu(menu) |
| _select(_tree, _tree.get_children()[0]) |
| |
| |
| def _enter_menu(menu): |
| # Enters the menu 'menu'. Used in single-menu mode. |
| |
| global _cur_menu |
| |
| _cur_menu = menu |
| _update_tree() |
| |
| _backbutton["state"] = "disabled" if menu is _kconf.top_node else "normal" |
| |
| |
| def _leave_menu(): |
| # Leaves the current menu. Used in single-menu mode. |
| |
| global _cur_menu |
| |
| if _cur_menu is not _kconf.top_node: |
| old_menu = _cur_menu |
| |
| _cur_menu = _parent_menu(_cur_menu) |
| _update_tree() |
| |
| _select(_tree, id(old_menu)) |
| |
| if _cur_menu is _kconf.top_node: |
| _backbutton["state"] = "disabled" |
| |
| _tree.focus_set() |
| |
| |
| def _select(tree, item): |
| # Selects, focuses, and see()s 'item' in 'tree' |
| |
| tree.selection_set(item) |
| tree.focus(item) |
| tree.see(item) |
| |
| |
| def _loc_ref_item(): |
| # Returns a Treeview item that can serve as a reference for the current |
| # scroll location. We try to make this item stay on the same row on the |
| # screen when updating the tree. |
| |
| # If the selected item is visible, use that |
| sel = _tree.selection() |
| if sel and _tree.bbox(sel[0]): |
| return sel[0] |
| |
| # Otherwise, use the middle item on the screen. If it doesn't exist, the |
| # tree is probably really small, so use the first item in the entire tree. |
| return _tree.identify_row(_tree.winfo_height()//2) or \ |
| _tree.get_children()[0] |
| |
| |
| def _vis_loc_ref_item(): |
| # Like _loc_ref_item(), but finds a visible item around the reference item. |
| # Used when changing show-all mode, where non-visible (red) items will |
| # disappear. |
| |
| item = _loc_ref_item() |
| |
| vis_before = _vis_before(item) |
| if vis_before and _tree.bbox(vis_before): |
| return vis_before |
| |
| vis_after = _vis_after(item) |
| if vis_after and _tree.bbox(vis_after): |
| return vis_after |
| |
| return vis_before or vis_after |
| |
| |
| def _vis_before(item): |
| # _vis_loc_ref_item() helper. Returns the first visible (not red) item, |
| # searching backwards from 'item'. |
| |
| while item: |
| if not _tree.tag_has("invisible", item): |
| return item |
| |
| prev = _tree.prev(item) |
| item = prev if prev else _tree.parent(item) |
| |
| return None |
| |
| |
| def _vis_after(item): |
| # _vis_loc_ref_item() helper. Returns the first visible (not red) item, |
| # searching forwards from 'item'. |
| |
| while item: |
| if not _tree.tag_has("invisible", item): |
| return item |
| |
| next = _tree.next(item) |
| if next: |
| item = next |
| else: |
| item = _tree.parent(item) |
| if not item: |
| break |
| item = _tree.next(item) |
| |
| return None |
| |
| |
| def _on_quit(_=None): |
| # Called when the user wants to exit |
| |
| if not _conf_changed: |
| _quit("No changes to save (for '{}')".format(_conf_filename)) |
| return |
| |
| while True: |
| ync = messagebox.askyesnocancel("Quit", "Save changes?") |
| if ync is None: |
| return |
| |
| if not ync: |
| _quit("Configuration ({}) was not saved".format(_conf_filename)) |
| return |
| |
| if _try_save(_kconf.write_config, _conf_filename, "configuration"): |
| # _try_save() already prints the "Configuration saved to ..." |
| # message |
| _quit() |
| return |
| |
| |
| def _quit(msg=None): |
| # Quits the application |
| |
| # Do not call sys.exit() here, in case we're being run from a script |
| _root.destroy() |
| if msg: |
| print(msg) |
| |
| |
| def _try_save(save_fn, filename, description): |
| # Tries to save a configuration file. Pops up an error and returns False on |
| # failure. |
| # |
| # save_fn: |
| # Function to call with 'filename' to save the file |
| # |
| # description: |
| # String describing the thing being saved |
| |
| try: |
| # save_fn() returns a message to print |
| msg = save_fn(filename) |
| _set_status(msg) |
| print(msg) |
| return True |
| except EnvironmentError as e: |
| messagebox.showerror( |
| "Error saving " + description, |
| "Error saving {} to '{}': {} (errno: {})" |
| .format(description, e.filename, e.strerror, |
| errno.errorcode[e.errno])) |
| return False |
| |
| |
| def _try_load(filename): |
| # Tries to load a configuration file. Pops up an error and returns False on |
| # failure. |
| # |
| # filename: |
| # Configuration file to load |
| |
| try: |
| msg = _kconf.load_config(filename) |
| _set_status(msg) |
| print(msg) |
| return True |
| except EnvironmentError as e: |
| messagebox.showerror( |
| "Error loading configuration", |
| "Error loading '{}': {} (errno: {})" |
| .format(filename, e.strerror, errno.errorcode[e.errno])) |
| return False |
| |
| |
| def _jump_to_dialog(_=None): |
| # Pops up a dialog for jumping directly to a particular node. Symbol values |
| # can also be changed within the dialog. |
| # |
| # Note: There's nothing preventing this from doing an incremental search |
| # like menuconfig.py does, but currently it's a bit jerky for large Kconfig |
| # trees, at least when inputting the beginning of the search string. We'd |
| # need to somehow only update the tree items that are shown in the Treeview |
| # to fix it. |
| |
| global _jump_to_tree |
| |
| def search(_=None): |
| _update_jump_to_matches(msglabel, entry.get()) |
| |
| def jump_to_selected(event=None): |
| # Jumps to the selected node and closes the dialog |
| |
| # Ignore double clicks on the image and in the heading area |
| if event and (tree.identify_element(event.x, event.y) == "image" or |
| _in_heading(event)): |
| return |
| |
| sel = tree.selection() |
| if not sel: |
| return |
| |
| node = _id_to_node[sel[0]] |
| |
| if node not in _shown_menu_nodes(_parent_menu(node)): |
| _show_all_var.set(True) |
| if not _single_menu: |
| # See comment in _do_tree_mode() |
| _update_tree() |
| |
| _jump_to(node) |
| |
| dialog.destroy() |
| |
| def tree_select(_): |
| jumpto_button["state"] = "normal" if tree.selection() else "disabled" |
| |
| |
| dialog = Toplevel(_root) |
| dialog.geometry("+{}+{}".format( |
| _root.winfo_rootx() + 50, _root.winfo_rooty() + 50)) |
| dialog.title("Jump to symbol/choice/menu/comment") |
| dialog.minsize(128, 128) # See _create_ui() |
| dialog.transient(_root) |
| |
| ttk.Label(dialog, text=_JUMP_TO_HELP) \ |
| .grid(column=0, row=0, columnspan=2, sticky="w", padx=".1c", |
| pady=".1c") |
| |
| entry = ttk.Entry(dialog) |
| entry.grid(column=0, row=1, sticky="ew", padx=".1c", pady=".1c") |
| entry.focus_set() |
| |
| entry.bind("<Return>", search) |
| entry.bind("<KP_Enter>", search) |
| |
| ttk.Button(dialog, text="Search", command=search) \ |
| .grid(column=1, row=1, padx="0 .1c", pady="0 .1c") |
| |
| msglabel = ttk.Label(dialog) |
| msglabel.grid(column=0, row=2, sticky="w", pady="0 .1c") |
| |
| panedwindow, tree = _create_kconfig_tree_and_desc(dialog) |
| panedwindow.grid(column=0, row=3, columnspan=2, sticky="nsew") |
| |
| # Clear tree |
| tree.set_children("") |
| |
| _jump_to_tree = tree |
| |
| jumpto_button = ttk.Button(dialog, text="Jump to selected item", |
| state="disabled", command=jump_to_selected) |
| jumpto_button.grid(column=0, row=4, columnspan=2, sticky="ns", pady=".1c") |
| |
| dialog.columnconfigure(0, weight=1) |
| # Only the pane with the Kconfig tree and description grows vertically |
| dialog.rowconfigure(3, weight=1) |
| |
| # See the menuconfig() function |
| _root.update_idletasks() |
| dialog.geometry(dialog.geometry()) |
| |
| # The dialog must be visible before we can grab the input |
| dialog.wait_visibility() |
| dialog.grab_set() |
| |
| tree.bind("<Double-1>", jump_to_selected) |
| tree.bind("<Return>", jump_to_selected) |
| tree.bind("<KP_Enter>", jump_to_selected) |
| # add=True to avoid overriding the description text update |
| tree.bind("<<TreeviewSelect>>", tree_select, add=True) |
| |
| dialog.bind("<Escape>", lambda _: dialog.destroy()) |
| |
| # Wait for the user to be done with the dialog |
| _root.wait_window(dialog) |
| |
| _jump_to_tree = None |
| |
| _tree.focus_set() |
| |
| |
| def _update_jump_to_matches(msglabel, search_string): |
| # Searches for nodes matching the search string and updates |
| # _jump_to_matches. Puts a message in 'msglabel' if there are no matches, |
| # or regex errors. |
| |
| global _jump_to_matches |
| |
| _jump_to_tree.selection_set(()) |
| |
| try: |
| # We could use re.IGNORECASE here instead of lower(), but this is |
| # faster for regexes like '.*debug$' (though the '.*' is redundant |
| # there). Those probably have bad interactions with re.search(), which |
| # matches anywhere in the string. |
| regex_searches = [re.compile(regex).search |
| for regex in search_string.lower().split()] |
| except re.error as e: |
| msg = "Bad regular expression" |
| # re.error.msg was added in Python 3.5 |
| if hasattr(e, "msg"): |
| msg += ": " + e.msg |
| msglabel["text"] = msg |
| # Clear tree |
| _jump_to_tree.set_children("") |
| return |
| |
| _jump_to_matches = [] |
| add_match = _jump_to_matches.append |
| |
| for node in _sorted_sc_nodes(): |
| # Symbol/choice |
| sc = node.item |
| |
| for search in regex_searches: |
| # Both the name and the prompt might be missing, since |
| # we're searching both symbols and choices |
| |
| # Does the regex match either the symbol name or the |
| # prompt (if any)? |
| if not (sc.name and search(sc.name.lower()) or |
| node.prompt and search(node.prompt[0].lower())): |
| |
| # Give up on the first regex that doesn't match, to |
| # speed things up a bit when multiple regexes are |
| # entered |
| break |
| |
| else: |
| add_match(node) |
| |
| # Search menus and comments |
| |
| for node in _sorted_menu_comment_nodes(): |
| for search in regex_searches: |
| if not search(node.prompt[0].lower()): |
| break |
| else: |
| add_match(node) |
| |
| msglabel["text"] = "" if _jump_to_matches else "No matches" |
| |
| _update_jump_to_display() |
| |
| if _jump_to_matches: |
| item = id(_jump_to_matches[0]) |
| _jump_to_tree.selection_set(item) |
| _jump_to_tree.focus(item) |
| |
| |
| def _update_jump_to_display(): |
| # Updates the images and text for the items in _jump_to_matches, and sets |
| # them as the items of _jump_to_tree |
| |
| # Micro-optimize a bit |
| item = _jump_to_tree.item |
| id_ = id |
| node_str = _node_str |
| img_tag = _img_tag |
| visible = _visible |
| for node in _jump_to_matches: |
| item(id_(node), |
| text=node_str(node), |
| tags=img_tag(node) if visible(node) else |
| img_tag(node) + " invisible") |
| |
| _jump_to_tree.set_children("", *map(id, _jump_to_matches)) |
| |
| |
| def _jump_to(node): |
| # Jumps directly to 'node' and selects it |
| |
| if _single_menu: |
| _enter_menu(_parent_menu(node)) |
| else: |
| _load_parents(node) |
| |
| _select(_tree, id(node)) |
| |
| |
| # Obscure Python: We never pass a value for cached_nodes, and it keeps pointing |
| # to the same list. This avoids a global. |
| def _sorted_sc_nodes(cached_nodes=[]): |
| # Returns a sorted list of symbol and choice nodes to search. The symbol |
| # nodes appear first, sorted by name, and then the choice nodes, sorted by |
| # prompt and (secondarily) name. |
| |
| if not cached_nodes: |
| # Add symbol nodes |
| for sym in sorted(_kconf.unique_defined_syms, |
| key=lambda sym: sym.name): |
| # += is in-place for lists |
| cached_nodes += sym.nodes |
| |
| # Add choice nodes |
| |
| choices = sorted(_kconf.unique_choices, |
| key=lambda choice: choice.name or "") |
| |
| cached_nodes += sorted( |
| [node for choice in choices for node in choice.nodes], |
| key=lambda node: node.prompt[0] if node.prompt else "") |
| |
| return cached_nodes |
| |
| |
| def _sorted_menu_comment_nodes(cached_nodes=[]): |
| # Returns a list of menu and comment nodes to search, sorted by prompt, |
| # with the menus first |
| |
| if not cached_nodes: |
| def prompt_text(mc): |
| return mc.prompt[0] |
| |
| cached_nodes += sorted(_kconf.menus, key=prompt_text) |
| cached_nodes += sorted(_kconf.comments, key=prompt_text) |
| |
| return cached_nodes |
| |
| |
| def _load_parents(node): |
| # Menus are lazily populated as they're opened in full-tree mode, but |
| # jumping to an item needs its parent menus to be populated. This function |
| # populates 'node's parents. |
| |
| # Get all parents leading up to 'node', sorted with the root first |
| parents = [] |
| cur = node.parent |
| while cur is not _kconf.top_node: |
| parents.append(cur) |
| cur = cur.parent |
| parents.reverse() |
| |
| for i, parent in enumerate(parents): |
| if not _tree.item(id(parent), "open"): |
| # Found a closed menu. Populate it and all the remaining menus |
| # leading up to 'node'. |
| for parent in parents[i:]: |
| # We only need to populate "real" menus/choices. Implicit menus |
| # are populated when their parents menus are entered. |
| if not isinstance(parent.item, Symbol): |
| _build_full_tree(parent) |
| return |
| |
| |
| def _parent_menu(node): |
| # Returns the menu node of the menu that contains 'node'. In addition to |
| # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. |
| # "Menu" here means a menu in the interface. |
| |
| menu = node.parent |
| while not menu.is_menuconfig: |
| menu = menu.parent |
| return menu |
| |
| |
| def _trace_write(var, fn): |
| # Makes fn() be called whenever the Tkinter Variable 'var' changes value |
| |
| # trace_variable() is deprecated according to the docstring, |
| # which recommends trace_add() |
| if hasattr(var, "trace_add"): |
| var.trace_add("write", fn) |
| else: |
| var.trace_variable("w", fn) |
| |
| |
| def _info_str(node): |
| # Returns information about the menu node 'node' as a string. |
| # |
| # The helper functions are responsible for adding newlines. This allows |
| # them to return "" if they don't want to add any output. |
| |
| if isinstance(node.item, Symbol): |
| sym = node.item |
| |
| return ( |
| _name_info(sym) + |
| _help_info(sym) + |
| _direct_dep_info(sym) + |
| _defaults_info(sym) + |
| _select_imply_info(sym) + |
| _kconfig_def_info(sym) |
| ) |
| |
| if isinstance(node.item, Choice): |
| choice = node.item |
| |
| return ( |
| _name_info(choice) + |
| _help_info(choice) + |
| 'Mode: {}\n\n'.format(choice.str_value) + |
| _choice_syms_info(choice) + |
| _direct_dep_info(choice) + |
| _defaults_info(choice) + |
| _kconfig_def_info(choice) |
| ) |
| |
| # node.item in (MENU, COMMENT) |
| return _kconfig_def_info(node) |
| |
| |
| def _name_info(sc): |
| # Returns a string with the name of the symbol/choice. Choices are shown as |
| # <choice (name if any)>. |
| |
| return (sc.name if sc.name else standard_sc_expr_str(sc)) + "\n\n" |
| |
| |
| def _value_info(sym): |
| # Returns a string showing 'sym's value |
| |
| # Only put quotes around the value for string symbols |
| return "Value: {}\n".format( |
| '"{}"'.format(sym.str_value) |
| if sym.orig_type == STRING |
| else sym.str_value) |
| |
| |
| def _choice_syms_info(choice): |
| # Returns a string listing the choice symbols in 'choice'. Adds |
| # "(selected)" next to the selected one. |
| |
| s = "Choice symbols:\n" |
| |
| for sym in choice.syms: |
| s += " - " + sym.name |
| if sym is choice.selection: |
| s += " (selected)" |
| s += "\n" |
| |
| return s + "\n" |
| |
| |
| def _help_info(sc): |
| # Returns a string with the help text(s) of 'sc' (Symbol or Choice). |
| # Symbols and choices defined in multiple locations can have multiple help |
| # texts. |
| |
| s = "" |
| |
| for node in sc.nodes: |
| if node.help is not None: |
| s += node.help + "\n\n" |
| |
| return s |
| |
| |
| def _direct_dep_info(sc): |
| # Returns a string describing the direct dependencies of 'sc' (Symbol or |
| # Choice). The direct dependencies are the OR of the dependencies from each |
| # definition location. The dependencies at each definition location come |
| # from 'depends on' and dependencies inherited from parent items. |
| |
| return "" if sc.direct_dep is _kconf.y else \ |
| 'Direct dependencies (={}):\n{}\n' \ |
| .format(TRI_TO_STR[expr_value(sc.direct_dep)], |
| _split_expr_info(sc.direct_dep, 2)) |
| |
| |
| def _defaults_info(sc): |
| # Returns a string describing the defaults of 'sc' (Symbol or Choice) |
| |
| if not sc.defaults: |
| return "" |
| |
| s = "Default" |
| if len(sc.defaults) > 1: |
| s += "s" |
| s += ":\n" |
| |
| for val, cond in sc.orig_defaults: |
| s += " - " |
| if isinstance(sc, Symbol): |
| s += _expr_str(val) |
| |
| # Skip the tristate value hint if the expression is just a single |
| # symbol. _expr_str() already shows its value as a string. |
| # |
| # This also avoids showing the tristate value for string/int/hex |
| # defaults, which wouldn't make any sense. |
| if isinstance(val, tuple): |
| s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) |
| else: |
| # Don't print the value next to the symbol name for choice |
| # defaults, as it looks a bit confusing |
| s += val.name |
| s += "\n" |
| |
| if cond is not _kconf.y: |
| s += " Condition (={}):\n{}" \ |
| .format(TRI_TO_STR[expr_value(cond)], |
| _split_expr_info(cond, 4)) |
| |
| return s + "\n" |
| |
| |
| def _split_expr_info(expr, indent): |
| # Returns a string with 'expr' split into its top-level && or || operands, |
| # with one operand per line, together with the operand's value. This is |
| # usually enough to get something readable for long expressions. A fancier |
| # recursive thingy would be possible too. |
| # |
| # indent: |
| # Number of leading spaces to add before the split expression. |
| |
| if len(split_expr(expr, AND)) > 1: |
| split_op = AND |
| op_str = "&&" |
| else: |
| split_op = OR |
| op_str = "||" |
| |
| s = "" |
| for i, term in enumerate(split_expr(expr, split_op)): |
| s += "{}{} {}".format(indent*" ", |
| " " if i == 0 else op_str, |
| _expr_str(term)) |
| |
| # Don't bother showing the value hint if the expression is just a |
| # single symbol. _expr_str() already shows its value. |
| if isinstance(term, tuple): |
| s += " (={})".format(TRI_TO_STR[expr_value(term)]) |
| |
| s += "\n" |
| |
| return s |
| |
| |
| def _select_imply_info(sym): |
| # Returns a string with information about which symbols 'select' or 'imply' |
| # 'sym'. The selecting/implying symbols are grouped according to which |
| # value they select/imply 'sym' to (n/m/y). |
| |
| def sis(expr, val, title): |
| # sis = selects/implies |
| sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] |
| if not sis: |
| return "" |
| |
| res = title |
| for si in sis: |
| res += " - {}\n".format(split_expr(si, AND)[0].name) |
| return res + "\n" |
| |
| s = "" |
| |
| if sym.rev_dep is not _kconf.n: |
| s += sis(sym.rev_dep, 2, |
| "Symbols currently y-selecting this symbol:\n") |
| s += sis(sym.rev_dep, 1, |
| "Symbols currently m-selecting this symbol:\n") |
| s += sis(sym.rev_dep, 0, |
| "Symbols currently n-selecting this symbol (no effect):\n") |
| |
| if sym.weak_rev_dep is not _kconf.n: |
| s += sis(sym.weak_rev_dep, 2, |
| "Symbols currently y-implying this symbol:\n") |
| s += sis(sym.weak_rev_dep, 1, |
| "Symbols currently m-implying this symbol:\n") |
| s += sis(sym.weak_rev_dep, 0, |
| "Symbols currently n-implying this symbol (no effect):\n") |
| |
| return s |
| |
| |
| def _kconfig_def_info(item): |
| # Returns a string with the definition of 'item' in Kconfig syntax, |
| # together with the definition location(s) and their include and menu paths |
| |
| nodes = [item] if isinstance(item, MenuNode) else item.nodes |
| |
| s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ |
| .format("s" if len(nodes) > 1 else "") |
| s += (len(s) - 1)*"=" |
| |
| for node in nodes: |
| s += "\n\n" \ |
| "At {}:{}\n" \ |
| "{}" \ |
| "Menu path: {}\n\n" \ |
| "{}" \ |
| .format(node.filename, node.linenr, |
| _include_path_info(node), |
| _menu_path_info(node), |
| node.custom_str(_name_and_val_str)) |
| |
| return s |
| |
| |
| def _include_path_info(node): |
| if not node.include_path: |
| # In the top-level Kconfig file |
| return "" |
| |
| return "Included via {}\n".format( |
| " -> ".join("{}:{}".format(filename, linenr) |
| for filename, linenr in node.include_path)) |
| |
| |
| def _menu_path_info(node): |
| # Returns a string describing the menu path leading up to 'node' |
| |
| path = "" |
| |
| while node.parent is not _kconf.top_node: |
| node = node.parent |
| |
| # Promptless choices might appear among the parents. Use |
| # standard_sc_expr_str() for them, so that they show up as |
| # '<choice (name if any)>'. |
| path = " -> " + (node.prompt[0] if node.prompt else |
| standard_sc_expr_str(node.item)) + path |
| |
| return "(Top)" + path |
| |
| |
| def _name_and_val_str(sc): |
| # Custom symbol/choice printer that shows symbol values after symbols |
| |
| # Show the values of non-constant (non-quoted) symbols that don't look like |
| # numbers. Things like 123 are actually symbol references, and only work as |
| # expected due to undefined symbols getting their name as their value. |
| # Showing the symbol value for those isn't helpful though. |
| if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): |
| if not sc.nodes: |
| # Undefined symbol reference |
| return "{}(undefined/n)".format(sc.name) |
| |
| return '{}(={})'.format(sc.name, sc.str_value) |
| |
| # For other items, use the standard format |
| return standard_sc_expr_str(sc) |
| |
| |
| def _expr_str(expr): |
| # Custom expression printer that shows symbol values |
| return expr_str(expr, _name_and_val_str) |
| |
| |
| def _is_num(name): |
| # Heuristic to see if a symbol name looks like a number, for nicer output |
| # when printing expressions. Things like 16 are actually symbol names, only |
| # they get their name as their value when the symbol is undefined. |
| |
| try: |
| int(name) |
| except ValueError: |
| if not name.startswith(("0x", "0X")): |
| return False |
| |
| try: |
| int(name, 16) |
| except ValueError: |
| return False |
| |
| return True |
| |
| |
| if __name__ == "__main__": |
| _main() |