You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2331 lines
72 KiB
2331 lines
72 KiB
#!/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()
|
|
|