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.
2324 lines
72 KiB
2324 lines
72 KiB
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) 2019, Ulf Magnusson
|
|
# SPDX-License-Identifier: ISC
|
|
|
|
"""
|
|
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 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="none", 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()
|
|
|