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