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