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