1*10465441SEvalZero# SPDX-License-Identifier: ISC 2*10465441SEvalZero# -*- coding: utf-8 -*- 3*10465441SEvalZero 4*10465441SEvalZero""" 5*10465441SEvalZeroOverview 6*10465441SEvalZero======== 7*10465441SEvalZero 8*10465441SEvalZeropymenuconfig is a small and simple frontend to Kconfiglib that's written 9*10465441SEvalZeroentirely in Python using Tkinter as its GUI toolkit. 10*10465441SEvalZero 11*10465441SEvalZeroMotivation 12*10465441SEvalZero========== 13*10465441SEvalZero 14*10465441SEvalZeroKconfig is a nice and powerful framework for build-time configuration and lots 15*10465441SEvalZeroof projects already benefit from using it. Kconfiglib allows to utilize power of 16*10465441SEvalZeroKconfig by using scripts written in pure Python, without requiring one to build 17*10465441SEvalZeroLinux kernel tools written in C (this can be quite tedious on anything that's 18*10465441SEvalZeronot *nix). The aim of this project is to implement simple and small Kconfiglib 19*10465441SEvalZeroGUI frontend that runs on as much systems as possible. 20*10465441SEvalZero 21*10465441SEvalZeroTkinter GUI toolkit is a natural choice if portability is considered, as it's 22*10465441SEvalZeroa part of Python standard library and is available virtually in every CPython 23*10465441SEvalZeroinstallation. 24*10465441SEvalZero 25*10465441SEvalZero 26*10465441SEvalZeroUser interface 27*10465441SEvalZero============== 28*10465441SEvalZero 29*10465441SEvalZeroI've tried to replicate look and fill of Linux kernel 'menuconfig' tool that 30*10465441SEvalZeromany users are used to, including keyboard-oriented control and textual 31*10465441SEvalZerorepresentation of menus with fixed-width font. 32*10465441SEvalZero 33*10465441SEvalZero 34*10465441SEvalZeroUsage 35*10465441SEvalZero===== 36*10465441SEvalZero 37*10465441SEvalZeroThe pymenuconfig module is executable and parses command-line args, so the 38*10465441SEvalZeromost simple way to run menuconfig is to execute script directly: 39*10465441SEvalZero 40*10465441SEvalZero python pymenuconfig.py --kconfig Kconfig 41*10465441SEvalZero 42*10465441SEvalZeroAs with most command-line tools list of options can be obtained with '--help': 43*10465441SEvalZero 44*10465441SEvalZero python pymenuconfig.py --help 45*10465441SEvalZero 46*10465441SEvalZeroIf installed with setuptools, one can run it like this: 47*10465441SEvalZero 48*10465441SEvalZero python -m pymenuconfig --kconfig Kconfig 49*10465441SEvalZero 50*10465441SEvalZeroIn case you're making a wrapper around menuconfig, you can either call main(): 51*10465441SEvalZero 52*10465441SEvalZero import pymenuconfig 53*10465441SEvalZero pymenuconfig.main(['--kconfig', 'Kconfig']) 54*10465441SEvalZero 55*10465441SEvalZeroOr import MenuConfig class, instantiate it and manually run Tkinter's mainloop: 56*10465441SEvalZero 57*10465441SEvalZero import tkinter 58*10465441SEvalZero import kconfiglib 59*10465441SEvalZero from pymenuconfig import MenuConfig 60*10465441SEvalZero 61*10465441SEvalZero kconfig = kconfiglib.Kconfig() 62*10465441SEvalZero mconf = MenuConfig(kconfig) 63*10465441SEvalZero tkinter.mainloop() 64*10465441SEvalZero 65*10465441SEvalZero""" 66*10465441SEvalZero 67*10465441SEvalZerofrom __future__ import print_function 68*10465441SEvalZero 69*10465441SEvalZeroimport os 70*10465441SEvalZeroimport sys 71*10465441SEvalZeroimport argparse 72*10465441SEvalZeroimport kconfiglib 73*10465441SEvalZero 74*10465441SEvalZero# Tk is imported differently depending on python major version 75*10465441SEvalZeroif sys.version_info[0] < 3: 76*10465441SEvalZero import Tkinter as tk 77*10465441SEvalZero import tkFont as font 78*10465441SEvalZero import tkFileDialog as filedialog 79*10465441SEvalZero import tkMessageBox as messagebox 80*10465441SEvalZeroelse: 81*10465441SEvalZero import tkinter as tk 82*10465441SEvalZero from tkinter import font 83*10465441SEvalZero from tkinter import filedialog 84*10465441SEvalZero from tkinter import messagebox 85*10465441SEvalZero 86*10465441SEvalZero 87*10465441SEvalZeroclass ListEntry(object): 88*10465441SEvalZero """ 89*10465441SEvalZero Represents visible menu node and holds all information related to displaying 90*10465441SEvalZero menu node in a Listbox. 91*10465441SEvalZero 92*10465441SEvalZero Instances of this class also handle all interaction with main window. 93*10465441SEvalZero A node is displayed as a single line of text: 94*10465441SEvalZero PREFIX INDENT BODY POSTFIX 95*10465441SEvalZero - The PREFIX is always 3 characters or more and can take following values: 96*10465441SEvalZero ' ' comment, menu, bool choice, etc. 97*10465441SEvalZero Inside menus: 98*10465441SEvalZero '< >' bool symbol has value 'n' 99*10465441SEvalZero '<*>' bool symbol has value 'y' 100*10465441SEvalZero '[ ]' tristate symbol has value 'n' 101*10465441SEvalZero '[M]' tristate symbol has value 'm' 102*10465441SEvalZero '[*]' tristate symbol has value 'y' 103*10465441SEvalZero '- -' symbol has value 'n' that's not editable 104*10465441SEvalZero '-M-' symbol has value 'm' that's not editable 105*10465441SEvalZero '-*-' symbol has value 'y' that's not editable 106*10465441SEvalZero '(M)' tristate choice has value 'm' 107*10465441SEvalZero '(*)' tristate choice has value 'y' 108*10465441SEvalZero '(some value)' value of non-bool/tristate symbols 109*10465441SEvalZero Inside choices: 110*10465441SEvalZero '( )' symbol has value 'n' 111*10465441SEvalZero '(M)' symbol has value 'm' 112*10465441SEvalZero '(*)' symbol has value 'y' 113*10465441SEvalZero - INDENT is a sequence of space characters. It's used in implicit menus, and 114*10465441SEvalZero adds 2 spaces for each nesting level 115*10465441SEvalZero - BODY is a menu node prompt. '***' is added if node is a comment 116*10465441SEvalZero - POSTFIX adds '(NEW)', '--->' and selected choice symbol where applicable 117*10465441SEvalZero 118*10465441SEvalZero Attributes: 119*10465441SEvalZero 120*10465441SEvalZero node: 121*10465441SEvalZero MenuNode instance this ListEntry is created for. 122*10465441SEvalZero 123*10465441SEvalZero visible: 124*10465441SEvalZero Whether entry should be shown in main window. 125*10465441SEvalZero 126*10465441SEvalZero text: 127*10465441SEvalZero String to display in a main window's Listbox. 128*10465441SEvalZero 129*10465441SEvalZero refresh(): 130*10465441SEvalZero Updates .visible and .text attribute values. 131*10465441SEvalZero 132*10465441SEvalZero set_tristate_value(): 133*10465441SEvalZero Set value for bool/tristate symbols, value should be one of 0,1,2 or None. 134*10465441SEvalZero Usually it's called when user presses 'y', 'n', 'm' key. 135*10465441SEvalZero 136*10465441SEvalZero set_str_value(): 137*10465441SEvalZero Set value for non-bool/tristate symbols, value is a string. Usually called 138*10465441SEvalZero with a value returned by one of MenuConfig.ask_for_* methods. 139*10465441SEvalZero 140*10465441SEvalZero toggle(): 141*10465441SEvalZero Toggle bool/tristate symbol value. Called when '<Space>' key is pressed in 142*10465441SEvalZero a main window. Also selects choice value. 143*10465441SEvalZero 144*10465441SEvalZero select(): 145*10465441SEvalZero Called when '<Return>' key is pressed in a main window with 'SELECT' 146*10465441SEvalZero action selected. Displays submenu, choice selection menu, or just selects 147*10465441SEvalZero choice value. For non-bool/tristate symbols asks MenuConfig window to 148*10465441SEvalZero handle value input via one of MenuConfig.ask_for_* methods. 149*10465441SEvalZero 150*10465441SEvalZero show_help(): 151*10465441SEvalZero Called when '<Return>' key is pressed in a main window with 'HELP' action 152*10465441SEvalZero selected. Prepares text help and calls MenuConfig.show_text() to display 153*10465441SEvalZero text window. 154*10465441SEvalZero """ 155*10465441SEvalZero 156*10465441SEvalZero # How to display value of BOOL and TRISTATE symbols 157*10465441SEvalZero TRI_TO_DISPLAY = { 158*10465441SEvalZero 0: ' ', 159*10465441SEvalZero 1: 'M', 160*10465441SEvalZero 2: '*' 161*10465441SEvalZero } 162*10465441SEvalZero 163*10465441SEvalZero def __init__(self, mconf, node, indent): 164*10465441SEvalZero self.indent = indent 165*10465441SEvalZero self.node = node 166*10465441SEvalZero self.menuconfig = mconf 167*10465441SEvalZero self.visible = False 168*10465441SEvalZero self.text = None 169*10465441SEvalZero 170*10465441SEvalZero def __str__(self): 171*10465441SEvalZero return self.text 172*10465441SEvalZero 173*10465441SEvalZero def _is_visible(self): 174*10465441SEvalZero node = self.node 175*10465441SEvalZero v = True 176*10465441SEvalZero v = v and node.prompt is not None 177*10465441SEvalZero # It should be enough to check if prompt expression is not false and 178*10465441SEvalZero # for menu nodes whether 'visible if' is not false 179*10465441SEvalZero v = v and kconfiglib.expr_value(node.prompt[1]) > 0 180*10465441SEvalZero if node.item == kconfiglib.MENU: 181*10465441SEvalZero v = v and kconfiglib.expr_value(node.visibility) > 0 182*10465441SEvalZero # If node references Symbol, then we also account for symbol visibility 183*10465441SEvalZero # TODO: need to re-think whether this is needed 184*10465441SEvalZero if isinstance(node.item, kconfiglib.Symbol): 185*10465441SEvalZero if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE): 186*10465441SEvalZero v = v and len(node.item.assignable) > 0 187*10465441SEvalZero else: 188*10465441SEvalZero v = v and node.item.visibility > 0 189*10465441SEvalZero return v 190*10465441SEvalZero 191*10465441SEvalZero def _get_text(self): 192*10465441SEvalZero """ 193*10465441SEvalZero Compute textual representation of menu node (a line in ListView) 194*10465441SEvalZero """ 195*10465441SEvalZero node = self.node 196*10465441SEvalZero item = node.item 197*10465441SEvalZero # Determine prefix 198*10465441SEvalZero prefix = ' ' 199*10465441SEvalZero if (isinstance(item, kconfiglib.Symbol) and item.choice is None or 200*10465441SEvalZero isinstance(item, kconfiglib.Choice) and item.type is kconfiglib.TRISTATE): 201*10465441SEvalZero # The node is for either a symbol outside of choice statement 202*10465441SEvalZero # or a tristate choice 203*10465441SEvalZero if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE): 204*10465441SEvalZero value = ListEntry.TRI_TO_DISPLAY[item.tri_value] 205*10465441SEvalZero if len(item.assignable) > 1: 206*10465441SEvalZero # Symbol is editable 207*10465441SEvalZero if 1 in item.assignable: 208*10465441SEvalZero prefix = '<{}>'.format(value) 209*10465441SEvalZero else: 210*10465441SEvalZero prefix = '[{}]'.format(value) 211*10465441SEvalZero else: 212*10465441SEvalZero # Symbol is not editable 213*10465441SEvalZero prefix = '-{}-'.format(value) 214*10465441SEvalZero else: 215*10465441SEvalZero prefix = '({})'.format(item.str_value) 216*10465441SEvalZero elif isinstance(item, kconfiglib.Symbol) and item.choice is not None: 217*10465441SEvalZero # The node is for symbol inside choice statement 218*10465441SEvalZero if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE): 219*10465441SEvalZero value = ListEntry.TRI_TO_DISPLAY[item.tri_value] 220*10465441SEvalZero if len(item.assignable) > 0: 221*10465441SEvalZero # Symbol is editable 222*10465441SEvalZero prefix = '({})'.format(value) 223*10465441SEvalZero else: 224*10465441SEvalZero # Symbol is not editable 225*10465441SEvalZero prefix = '-{}-'.format(value) 226*10465441SEvalZero else: 227*10465441SEvalZero prefix = '({})'.format(item.str_value) 228*10465441SEvalZero 229*10465441SEvalZero # Prefix should be at least 3 chars long 230*10465441SEvalZero if len(prefix) < 3: 231*10465441SEvalZero prefix += ' ' * (3 - len(prefix)) 232*10465441SEvalZero # Body 233*10465441SEvalZero body = '' 234*10465441SEvalZero if node.prompt is not None: 235*10465441SEvalZero if item is kconfiglib.COMMENT: 236*10465441SEvalZero body = '*** {} ***'.format(node.prompt[0]) 237*10465441SEvalZero else: 238*10465441SEvalZero body = node.prompt[0] 239*10465441SEvalZero # Suffix 240*10465441SEvalZero is_menu = False 241*10465441SEvalZero is_new = False 242*10465441SEvalZero if (item is kconfiglib.MENU 243*10465441SEvalZero or isinstance(item, kconfiglib.Symbol) and node.is_menuconfig 244*10465441SEvalZero or isinstance(item, kconfiglib.Choice)): 245*10465441SEvalZero is_menu = True 246*10465441SEvalZero if isinstance(item, kconfiglib.Symbol) and item.user_value is None: 247*10465441SEvalZero is_new = True 248*10465441SEvalZero # For symbol inside choice that has 'y' value, '(NEW)' is not displayed 249*10465441SEvalZero if (isinstance(item, kconfiglib.Symbol) 250*10465441SEvalZero and item.choice and item.choice.tri_value == 2): 251*10465441SEvalZero is_new = False 252*10465441SEvalZero # Choice selection - displayed only for choices which have 'y' value 253*10465441SEvalZero choice_selection = None 254*10465441SEvalZero if isinstance(item, kconfiglib.Choice) and node.item.str_value == 'y': 255*10465441SEvalZero choice_selection = '' 256*10465441SEvalZero if item.selection is not None: 257*10465441SEvalZero sym = item.selection 258*10465441SEvalZero if sym.nodes and sym.nodes[0].prompt is not None: 259*10465441SEvalZero choice_selection = sym.nodes[0].prompt[0] 260*10465441SEvalZero text = ' {prefix} {indent}{body}{choice}{new}{menu}'.format( 261*10465441SEvalZero prefix=prefix, 262*10465441SEvalZero indent=' ' * self.indent, 263*10465441SEvalZero body=body, 264*10465441SEvalZero choice='' if choice_selection is None else ' ({})'.format( 265*10465441SEvalZero choice_selection 266*10465441SEvalZero ), 267*10465441SEvalZero new=' (NEW)' if is_new else '', 268*10465441SEvalZero menu=' --->' if is_menu else '' 269*10465441SEvalZero ) 270*10465441SEvalZero return text 271*10465441SEvalZero 272*10465441SEvalZero def refresh(self): 273*10465441SEvalZero self.visible = self._is_visible() 274*10465441SEvalZero self.text = self._get_text() 275*10465441SEvalZero 276*10465441SEvalZero def set_tristate_value(self, value): 277*10465441SEvalZero """ 278*10465441SEvalZero Call to change value of BOOL, TRISTATE symbols 279*10465441SEvalZero 280*10465441SEvalZero It's preferred to use this instead of item.set_value as it handles 281*10465441SEvalZero all necessary interaction with MenuConfig window when symbol value 282*10465441SEvalZero changes 283*10465441SEvalZero 284*10465441SEvalZero None value is accepted but ignored 285*10465441SEvalZero """ 286*10465441SEvalZero item = self.node.item 287*10465441SEvalZero if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)) 288*10465441SEvalZero and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE) 289*10465441SEvalZero and value is not None): 290*10465441SEvalZero if value in item.assignable: 291*10465441SEvalZero item.set_value(value) 292*10465441SEvalZero elif value == 2 and 1 in item.assignable: 293*10465441SEvalZero print( 294*10465441SEvalZero 'Symbol {} value is limited to \'m\'. Setting value \'m\' instead of \'y\''.format(item.name), 295*10465441SEvalZero file=sys.stderr 296*10465441SEvalZero ) 297*10465441SEvalZero item.set_value(1) 298*10465441SEvalZero self.menuconfig.mark_as_changed() 299*10465441SEvalZero self.menuconfig.refresh_display() 300*10465441SEvalZero 301*10465441SEvalZero def set_str_value(self, value): 302*10465441SEvalZero """ 303*10465441SEvalZero Call to change value of HEX, INT, STRING symbols 304*10465441SEvalZero 305*10465441SEvalZero It's preferred to use this instead of item.set_value as it handles 306*10465441SEvalZero all necessary interaction with MenuConfig window when symbol value 307*10465441SEvalZero changes 308*10465441SEvalZero 309*10465441SEvalZero None value is accepted but ignored 310*10465441SEvalZero """ 311*10465441SEvalZero item = self.node.item 312*10465441SEvalZero if (isinstance(item, kconfiglib.Symbol) 313*10465441SEvalZero and item.type in (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING) 314*10465441SEvalZero and value is not None): 315*10465441SEvalZero item.set_value(value) 316*10465441SEvalZero self.menuconfig.mark_as_changed() 317*10465441SEvalZero self.menuconfig.refresh_display() 318*10465441SEvalZero 319*10465441SEvalZero def toggle(self): 320*10465441SEvalZero """ 321*10465441SEvalZero Called when <space> key is pressed 322*10465441SEvalZero """ 323*10465441SEvalZero item = self.node.item 324*10465441SEvalZero if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)) 325*10465441SEvalZero and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)): 326*10465441SEvalZero value = item.tri_value 327*10465441SEvalZero # Find next value in Symbol/Choice.assignable, or use assignable[0] 328*10465441SEvalZero try: 329*10465441SEvalZero it = iter(item.assignable) 330*10465441SEvalZero while value != next(it): 331*10465441SEvalZero pass 332*10465441SEvalZero self.set_tristate_value(next(it)) 333*10465441SEvalZero except StopIteration: 334*10465441SEvalZero self.set_tristate_value(item.assignable[0]) 335*10465441SEvalZero 336*10465441SEvalZero def select(self): 337*10465441SEvalZero """ 338*10465441SEvalZero Called when <Return> key is pressed and SELECT action is selected 339*10465441SEvalZero """ 340*10465441SEvalZero item = self.node.item 341*10465441SEvalZero # - Menu: dive into submenu 342*10465441SEvalZero # - INT, HEX, STRING symbol: raise prompt to enter symbol value 343*10465441SEvalZero # - BOOL, TRISTATE symbol inside 'y'-valued Choice: set 'y' value 344*10465441SEvalZero if (item is kconfiglib.MENU 345*10465441SEvalZero or isinstance(item, kconfiglib.Symbol) and self.node.is_menuconfig 346*10465441SEvalZero or isinstance(item, kconfiglib.Choice)): 347*10465441SEvalZero # Dive into submenu 348*10465441SEvalZero self.menuconfig.show_submenu(self.node) 349*10465441SEvalZero elif (isinstance(item, kconfiglib.Symbol) and item.type in 350*10465441SEvalZero (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)): 351*10465441SEvalZero # Raise prompt to enter symbol value 352*10465441SEvalZero ident = self.node.prompt[0] if self.node.prompt is not None else None 353*10465441SEvalZero title = 'Symbol: {}'.format(item.name) 354*10465441SEvalZero if item.type is kconfiglib.INT: 355*10465441SEvalZero # Find enabled ranges 356*10465441SEvalZero ranges = [ 357*10465441SEvalZero (int(start.str_value), int(end.str_value)) 358*10465441SEvalZero for start, end, expr in item.ranges 359*10465441SEvalZero if kconfiglib.expr_value(expr) > 0 360*10465441SEvalZero ] 361*10465441SEvalZero # Raise prompt 362*10465441SEvalZero self.set_str_value(str(self.menuconfig.ask_for_int( 363*10465441SEvalZero ident=ident, 364*10465441SEvalZero title=title, 365*10465441SEvalZero value=item.str_value, 366*10465441SEvalZero ranges=ranges 367*10465441SEvalZero ))) 368*10465441SEvalZero elif item.type is kconfiglib.HEX: 369*10465441SEvalZero # Find enabled ranges 370*10465441SEvalZero ranges = [ 371*10465441SEvalZero (int(start.str_value, base=16), int(end.str_value, base=16)) 372*10465441SEvalZero for start, end, expr in item.ranges 373*10465441SEvalZero if kconfiglib.expr_value(expr) > 0 374*10465441SEvalZero ] 375*10465441SEvalZero # Raise prompt 376*10465441SEvalZero self.set_str_value(hex(self.menuconfig.ask_for_hex( 377*10465441SEvalZero ident=ident, 378*10465441SEvalZero title=title, 379*10465441SEvalZero value=item.str_value, 380*10465441SEvalZero ranges=ranges 381*10465441SEvalZero ))) 382*10465441SEvalZero elif item.type is kconfiglib.STRING: 383*10465441SEvalZero # Raise prompt 384*10465441SEvalZero self.set_str_value(self.menuconfig.ask_for_string( 385*10465441SEvalZero ident=ident, 386*10465441SEvalZero title=title, 387*10465441SEvalZero value=item.str_value 388*10465441SEvalZero )) 389*10465441SEvalZero elif (isinstance(item, kconfiglib.Symbol) 390*10465441SEvalZero and item.choice is not None and item.choice.tri_value == 2): 391*10465441SEvalZero # Symbol inside choice -> set symbol value to 'y' 392*10465441SEvalZero self.set_tristate_value(2) 393*10465441SEvalZero 394*10465441SEvalZero def show_help(self): 395*10465441SEvalZero node = self.node 396*10465441SEvalZero item = self.node.item 397*10465441SEvalZero if isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)): 398*10465441SEvalZero title = 'Help for symbol: {}'.format(item.name) 399*10465441SEvalZero if node.help: 400*10465441SEvalZero help = node.help 401*10465441SEvalZero else: 402*10465441SEvalZero help = 'There is no help available for this option.\n' 403*10465441SEvalZero lines = [] 404*10465441SEvalZero lines.append(help) 405*10465441SEvalZero lines.append( 406*10465441SEvalZero 'Symbol: {} [={}]'.format( 407*10465441SEvalZero item.name if item.name else '<UNNAMED>', item.str_value 408*10465441SEvalZero ) 409*10465441SEvalZero ) 410*10465441SEvalZero lines.append('Type : {}'.format(kconfiglib.TYPE_TO_STR[item.type])) 411*10465441SEvalZero for n in item.nodes: 412*10465441SEvalZero lines.append('Prompt: {}'.format(n.prompt[0] if n.prompt else '<EMPTY>')) 413*10465441SEvalZero lines.append(' Defined at {}:{}'.format(n.filename, n.linenr)) 414*10465441SEvalZero lines.append(' Depends on: {}'.format(kconfiglib.expr_str(n.dep))) 415*10465441SEvalZero text = '\n'.join(lines) 416*10465441SEvalZero else: 417*10465441SEvalZero title = 'Help' 418*10465441SEvalZero text = 'Help not available for this menu node.\n' 419*10465441SEvalZero self.menuconfig.show_text(text, title) 420*10465441SEvalZero self.menuconfig.refresh_display() 421*10465441SEvalZero 422*10465441SEvalZero 423*10465441SEvalZeroclass EntryDialog(object): 424*10465441SEvalZero """ 425*10465441SEvalZero Creates modal dialog (top-level Tk window) with labels, entry box and two 426*10465441SEvalZero buttons: OK and CANCEL. 427*10465441SEvalZero """ 428*10465441SEvalZero def __init__(self, master, text, title, ident=None, value=None): 429*10465441SEvalZero self.master = master 430*10465441SEvalZero dlg = self.dlg = tk.Toplevel(master) 431*10465441SEvalZero self.dlg.withdraw() #hiden window 432*10465441SEvalZero dlg.title(title) 433*10465441SEvalZero # Identifier label 434*10465441SEvalZero if ident is not None: 435*10465441SEvalZero self.label_id = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT) 436*10465441SEvalZero self.label_id['font'] = font.nametofont('TkFixedFont') 437*10465441SEvalZero self.label_id['text'] = '# {}'.format(ident) 438*10465441SEvalZero self.label_id.pack(fill=tk.X, padx=2, pady=2) 439*10465441SEvalZero # Label 440*10465441SEvalZero self.label = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT) 441*10465441SEvalZero self.label['font'] = font.nametofont('TkFixedFont') 442*10465441SEvalZero self.label['text'] = text 443*10465441SEvalZero self.label.pack(fill=tk.X, padx=10, pady=4) 444*10465441SEvalZero # Entry box 445*10465441SEvalZero self.entry = tk.Entry(dlg) 446*10465441SEvalZero self.entry['font'] = font.nametofont('TkFixedFont') 447*10465441SEvalZero self.entry.pack(fill=tk.X, padx=2, pady=2) 448*10465441SEvalZero # Frame for buttons 449*10465441SEvalZero self.frame = tk.Frame(dlg) 450*10465441SEvalZero self.frame.pack(padx=2, pady=2) 451*10465441SEvalZero # Button 452*10465441SEvalZero self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept) 453*10465441SEvalZero self.btn_accept['font'] = font.nametofont('TkFixedFont') 454*10465441SEvalZero self.btn_accept.pack(side=tk.LEFT, padx=2) 455*10465441SEvalZero self.btn_cancel = tk.Button(self.frame, text='< Cancel >', command=self.cancel) 456*10465441SEvalZero self.btn_cancel['font'] = font.nametofont('TkFixedFont') 457*10465441SEvalZero self.btn_cancel.pack(side=tk.LEFT, padx=2) 458*10465441SEvalZero # Bind Enter and Esc keys 459*10465441SEvalZero self.dlg.bind('<Return>', self.accept) 460*10465441SEvalZero self.dlg.bind('<Escape>', self.cancel) 461*10465441SEvalZero # Dialog is resizable only by width 462*10465441SEvalZero self.dlg.resizable(1, 0) 463*10465441SEvalZero # Set supplied value (if any) 464*10465441SEvalZero if value is not None: 465*10465441SEvalZero self.entry.insert(0, value) 466*10465441SEvalZero self.entry.selection_range(0, tk.END) 467*10465441SEvalZero # By default returned value is None. To caller this means that entry 468*10465441SEvalZero # process was cancelled 469*10465441SEvalZero self.value = None 470*10465441SEvalZero # Modal dialog 471*10465441SEvalZero dlg.transient(master) 472*10465441SEvalZero dlg.grab_set() 473*10465441SEvalZero # Center dialog window 474*10465441SEvalZero _center_window_above_parent(master, dlg) 475*10465441SEvalZero self.dlg.deiconify() # show window 476*10465441SEvalZero # Focus entry field 477*10465441SEvalZero self.entry.focus_set() 478*10465441SEvalZero 479*10465441SEvalZero def accept(self, ev=None): 480*10465441SEvalZero self.value = self.entry.get() 481*10465441SEvalZero self.dlg.destroy() 482*10465441SEvalZero 483*10465441SEvalZero def cancel(self, ev=None): 484*10465441SEvalZero self.dlg.destroy() 485*10465441SEvalZero 486*10465441SEvalZero 487*10465441SEvalZeroclass TextDialog(object): 488*10465441SEvalZero def __init__(self, master, text, title): 489*10465441SEvalZero self.master = master 490*10465441SEvalZero dlg = self.dlg = tk.Toplevel(master) 491*10465441SEvalZero self.dlg.withdraw() #hiden window 492*10465441SEvalZero dlg.title(title) 493*10465441SEvalZero dlg.minsize(600,400) 494*10465441SEvalZero # Text 495*10465441SEvalZero self.text = tk.Text(dlg, height=1) 496*10465441SEvalZero self.text['font'] = font.nametofont('TkFixedFont') 497*10465441SEvalZero self.text.insert(tk.END, text) 498*10465441SEvalZero # Make text read-only 499*10465441SEvalZero self.text['state'] = tk.DISABLED 500*10465441SEvalZero self.text.pack(fill=tk.BOTH, expand=1, padx=4, pady=4) 501*10465441SEvalZero # Frame for buttons 502*10465441SEvalZero self.frame = tk.Frame(dlg) 503*10465441SEvalZero self.frame.pack(padx=2, pady=2) 504*10465441SEvalZero # Button 505*10465441SEvalZero self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept) 506*10465441SEvalZero self.btn_accept['font'] = font.nametofont('TkFixedFont') 507*10465441SEvalZero self.btn_accept.pack(side=tk.LEFT, padx=2) 508*10465441SEvalZero # Bind Enter and Esc keys 509*10465441SEvalZero self.dlg.bind('<Return>', self.accept) 510*10465441SEvalZero self.dlg.bind('<Escape>', self.cancel) 511*10465441SEvalZero # Modal dialog 512*10465441SEvalZero dlg.transient(master) 513*10465441SEvalZero dlg.grab_set() 514*10465441SEvalZero # Center dialog window 515*10465441SEvalZero _center_window_above_parent(master, dlg) 516*10465441SEvalZero self.dlg.deiconify() # show window 517*10465441SEvalZero # Focus entry field 518*10465441SEvalZero self.text.focus_set() 519*10465441SEvalZero 520*10465441SEvalZero def accept(self, ev=None): 521*10465441SEvalZero self.dlg.destroy() 522*10465441SEvalZero 523*10465441SEvalZero def cancel(self, ev=None): 524*10465441SEvalZero self.dlg.destroy() 525*10465441SEvalZero 526*10465441SEvalZero 527*10465441SEvalZeroclass MenuConfig(object): 528*10465441SEvalZero ( 529*10465441SEvalZero ACTION_SELECT, 530*10465441SEvalZero ACTION_EXIT, 531*10465441SEvalZero ACTION_HELP, 532*10465441SEvalZero ACTION_LOAD, 533*10465441SEvalZero ACTION_SAVE, 534*10465441SEvalZero ACTION_SAVE_AS 535*10465441SEvalZero ) = range(6) 536*10465441SEvalZero 537*10465441SEvalZero ACTIONS = ( 538*10465441SEvalZero ('Select', ACTION_SELECT), 539*10465441SEvalZero ('Exit', ACTION_EXIT), 540*10465441SEvalZero ('Help', ACTION_HELP), 541*10465441SEvalZero ('Load', ACTION_LOAD), 542*10465441SEvalZero ('Save', ACTION_SAVE), 543*10465441SEvalZero ('Save as', ACTION_SAVE_AS), 544*10465441SEvalZero ) 545*10465441SEvalZero 546*10465441SEvalZero def __init__(self, kconfig, __silent=None): 547*10465441SEvalZero self.kconfig = kconfig 548*10465441SEvalZero self.__silent = __silent 549*10465441SEvalZero if self.__silent is True: 550*10465441SEvalZero return 551*10465441SEvalZero 552*10465441SEvalZero # Instantiate Tk widgets 553*10465441SEvalZero self.root = tk.Tk() 554*10465441SEvalZero self.root.withdraw() #hiden window 555*10465441SEvalZero dlg = self.root 556*10465441SEvalZero 557*10465441SEvalZero # Window title 558*10465441SEvalZero dlg.title('pymenuconfig') 559*10465441SEvalZero # Some empirical window size 560*10465441SEvalZero dlg.minsize(500, 300) 561*10465441SEvalZero dlg.geometry('800x600') 562*10465441SEvalZero 563*10465441SEvalZero # Label that shows position in menu tree 564*10465441SEvalZero self.label_position = tk.Label( 565*10465441SEvalZero dlg, 566*10465441SEvalZero anchor=tk.W, 567*10465441SEvalZero justify=tk.LEFT, 568*10465441SEvalZero font=font.nametofont('TkFixedFont') 569*10465441SEvalZero ) 570*10465441SEvalZero self.label_position.pack(fill=tk.X, padx=2) 571*10465441SEvalZero 572*10465441SEvalZero # 'Tip' frame and text 573*10465441SEvalZero self.frame_tip = tk.LabelFrame( 574*10465441SEvalZero dlg, 575*10465441SEvalZero text='Tip' 576*10465441SEvalZero ) 577*10465441SEvalZero self.label_tip = tk.Label( 578*10465441SEvalZero self.frame_tip, 579*10465441SEvalZero anchor=tk.W, 580*10465441SEvalZero justify=tk.LEFT, 581*10465441SEvalZero font=font.nametofont('TkFixedFont') 582*10465441SEvalZero ) 583*10465441SEvalZero self.label_tip['text'] = '\n'.join([ 584*10465441SEvalZero 'Arrow keys navigate the menu. <Enter> performs selected operation (set of buttons at the bottom)', 585*10465441SEvalZero 'Pressing <Y> includes, <N> excludes, <M> modularizes features', 586*10465441SEvalZero 'Press <Esc> to go one level up. Press <Esc> at top level to exit', 587*10465441SEvalZero 'Legend: [*] built-in [ ] excluded <M> module < > module capable' 588*10465441SEvalZero ]) 589*10465441SEvalZero self.label_tip.pack(fill=tk.BOTH, expand=1, padx=4, pady=4) 590*10465441SEvalZero self.frame_tip.pack(fill=tk.X, padx=2) 591*10465441SEvalZero 592*10465441SEvalZero # Main ListBox where all the magic happens 593*10465441SEvalZero self.list = tk.Listbox( 594*10465441SEvalZero dlg, 595*10465441SEvalZero selectmode=tk.SINGLE, 596*10465441SEvalZero activestyle=tk.NONE, 597*10465441SEvalZero font=font.nametofont('TkFixedFont'), 598*10465441SEvalZero height=1, 599*10465441SEvalZero ) 600*10465441SEvalZero self.list['foreground'] = 'Blue' 601*10465441SEvalZero self.list['background'] = 'Gray95' 602*10465441SEvalZero # Make selection invisible 603*10465441SEvalZero self.list['selectbackground'] = self.list['background'] 604*10465441SEvalZero self.list['selectforeground'] = self.list['foreground'] 605*10465441SEvalZero self.list.pack(fill=tk.BOTH, expand=1, padx=20, ipadx=2) 606*10465441SEvalZero 607*10465441SEvalZero # Frame with radio buttons 608*10465441SEvalZero self.frame_radio = tk.Frame(dlg) 609*10465441SEvalZero self.radio_buttons = [] 610*10465441SEvalZero self.tk_selected_action = tk.IntVar() 611*10465441SEvalZero for text, value in MenuConfig.ACTIONS: 612*10465441SEvalZero btn = tk.Radiobutton( 613*10465441SEvalZero self.frame_radio, 614*10465441SEvalZero variable=self.tk_selected_action, 615*10465441SEvalZero value=value 616*10465441SEvalZero ) 617*10465441SEvalZero btn['text'] = '< {} >'.format(text) 618*10465441SEvalZero btn['font'] = font.nametofont('TkFixedFont') 619*10465441SEvalZero btn['indicatoron'] = 0 620*10465441SEvalZero btn.pack(side=tk.LEFT) 621*10465441SEvalZero self.radio_buttons.append(btn) 622*10465441SEvalZero self.frame_radio.pack(anchor=tk.CENTER, pady=4) 623*10465441SEvalZero # Label with status information 624*10465441SEvalZero self.tk_status = tk.StringVar() 625*10465441SEvalZero self.label_status = tk.Label( 626*10465441SEvalZero dlg, 627*10465441SEvalZero textvariable=self.tk_status, 628*10465441SEvalZero anchor=tk.W, 629*10465441SEvalZero justify=tk.LEFT, 630*10465441SEvalZero font=font.nametofont('TkFixedFont') 631*10465441SEvalZero ) 632*10465441SEvalZero self.label_status.pack(fill=tk.X, padx=4, pady=4) 633*10465441SEvalZero # Center window 634*10465441SEvalZero _center_window(self.root, dlg) 635*10465441SEvalZero self.root.deiconify() # show window 636*10465441SEvalZero # Disable keyboard focus on all widgets ... 637*10465441SEvalZero self._set_option_to_all_children(dlg, 'takefocus', 0) 638*10465441SEvalZero # ... except for main ListBox 639*10465441SEvalZero self.list['takefocus'] = 1 640*10465441SEvalZero self.list.focus_set() 641*10465441SEvalZero # Bind keys 642*10465441SEvalZero dlg.bind('<Escape>', self.handle_keypress) 643*10465441SEvalZero dlg.bind('<space>', self.handle_keypress) 644*10465441SEvalZero dlg.bind('<Return>', self.handle_keypress) 645*10465441SEvalZero dlg.bind('<Right>', self.handle_keypress) 646*10465441SEvalZero dlg.bind('<Left>', self.handle_keypress) 647*10465441SEvalZero dlg.bind('<Up>', self.handle_keypress) 648*10465441SEvalZero dlg.bind('<Down>', self.handle_keypress) 649*10465441SEvalZero dlg.bind('n', self.handle_keypress) 650*10465441SEvalZero dlg.bind('m', self.handle_keypress) 651*10465441SEvalZero dlg.bind('y', self.handle_keypress) 652*10465441SEvalZero # Register callback that's called when window closes 653*10465441SEvalZero dlg.wm_protocol('WM_DELETE_WINDOW', self._close_window) 654*10465441SEvalZero # Init fields 655*10465441SEvalZero self.node = None 656*10465441SEvalZero self.node_stack = [] 657*10465441SEvalZero self.all_entries = [] 658*10465441SEvalZero self.shown_entries = [] 659*10465441SEvalZero self.config_path = None 660*10465441SEvalZero self.unsaved_changes = False 661*10465441SEvalZero self.status_string = 'NEW CONFIG' 662*10465441SEvalZero self.update_status() 663*10465441SEvalZero # Display first child of top level node (the top level node is 'mainmenu') 664*10465441SEvalZero self.show_node(self.kconfig.top_node) 665*10465441SEvalZero 666*10465441SEvalZero def _set_option_to_all_children(self, widget, option, value): 667*10465441SEvalZero widget[option] = value 668*10465441SEvalZero for n,c in widget.children.items(): 669*10465441SEvalZero self._set_option_to_all_children(c, option, value) 670*10465441SEvalZero 671*10465441SEvalZero def _invert_colors(self, idx): 672*10465441SEvalZero self.list.itemconfig(idx, {'bg' : self.list['foreground']}) 673*10465441SEvalZero self.list.itemconfig(idx, {'fg' : self.list['background']}) 674*10465441SEvalZero 675*10465441SEvalZero @property 676*10465441SEvalZero def _selected_entry(self): 677*10465441SEvalZero # type: (...) -> ListEntry 678*10465441SEvalZero active_idx = self.list.index(tk.ACTIVE) 679*10465441SEvalZero if active_idx >= 0 and active_idx < len(self.shown_entries): 680*10465441SEvalZero return self.shown_entries[active_idx] 681*10465441SEvalZero return None 682*10465441SEvalZero 683*10465441SEvalZero def _select_node(self, node): 684*10465441SEvalZero # type: (kconfiglib.MenuNode) -> None 685*10465441SEvalZero """ 686*10465441SEvalZero Attempts to select entry that corresponds to given MenuNode in main listbox 687*10465441SEvalZero """ 688*10465441SEvalZero idx = None 689*10465441SEvalZero for i, e in enumerate(self.shown_entries): 690*10465441SEvalZero if e.node is node: 691*10465441SEvalZero idx = i 692*10465441SEvalZero break 693*10465441SEvalZero if idx is not None: 694*10465441SEvalZero self.list.activate(idx) 695*10465441SEvalZero self.list.see(idx) 696*10465441SEvalZero self._invert_colors(idx) 697*10465441SEvalZero 698*10465441SEvalZero def handle_keypress(self, ev): 699*10465441SEvalZero keysym = ev.keysym 700*10465441SEvalZero if keysym == 'Left': 701*10465441SEvalZero self._select_action(prev=True) 702*10465441SEvalZero elif keysym == 'Right': 703*10465441SEvalZero self._select_action(prev=False) 704*10465441SEvalZero elif keysym == 'Up': 705*10465441SEvalZero self.refresh_display(reset_selection=False) 706*10465441SEvalZero elif keysym == 'Down': 707*10465441SEvalZero self.refresh_display(reset_selection=False) 708*10465441SEvalZero elif keysym == 'space': 709*10465441SEvalZero self._selected_entry.toggle() 710*10465441SEvalZero elif keysym in ('n', 'm', 'y'): 711*10465441SEvalZero self._selected_entry.set_tristate_value(kconfiglib.STR_TO_TRI[keysym]) 712*10465441SEvalZero elif keysym == 'Return': 713*10465441SEvalZero action = self.tk_selected_action.get() 714*10465441SEvalZero if action == self.ACTION_SELECT: 715*10465441SEvalZero self._selected_entry.select() 716*10465441SEvalZero elif action == self.ACTION_EXIT: 717*10465441SEvalZero self._action_exit() 718*10465441SEvalZero elif action == self.ACTION_HELP: 719*10465441SEvalZero self._selected_entry.show_help() 720*10465441SEvalZero elif action == self.ACTION_LOAD: 721*10465441SEvalZero if self.prevent_losing_changes(): 722*10465441SEvalZero self.open_config() 723*10465441SEvalZero elif action == self.ACTION_SAVE: 724*10465441SEvalZero self.save_config() 725*10465441SEvalZero elif action == self.ACTION_SAVE_AS: 726*10465441SEvalZero self.save_config(force_file_dialog=True) 727*10465441SEvalZero elif keysym == 'Escape': 728*10465441SEvalZero self._action_exit() 729*10465441SEvalZero pass 730*10465441SEvalZero 731*10465441SEvalZero def _close_window(self): 732*10465441SEvalZero if self.prevent_losing_changes(): 733*10465441SEvalZero print('Exiting..') 734*10465441SEvalZero if self.__silent is True: 735*10465441SEvalZero return 736*10465441SEvalZero self.root.destroy() 737*10465441SEvalZero 738*10465441SEvalZero def _action_exit(self): 739*10465441SEvalZero if self.node_stack: 740*10465441SEvalZero self.show_parent() 741*10465441SEvalZero else: 742*10465441SEvalZero self._close_window() 743*10465441SEvalZero 744*10465441SEvalZero def _select_action(self, prev=False): 745*10465441SEvalZero # Determine the radio button that's activated 746*10465441SEvalZero action = self.tk_selected_action.get() 747*10465441SEvalZero if prev: 748*10465441SEvalZero action -= 1 749*10465441SEvalZero else: 750*10465441SEvalZero action += 1 751*10465441SEvalZero action %= len(MenuConfig.ACTIONS) 752*10465441SEvalZero self.tk_selected_action.set(action) 753*10465441SEvalZero 754*10465441SEvalZero def _collect_list_entries(self, start_node, indent=0): 755*10465441SEvalZero """ 756*10465441SEvalZero Given first MenuNode of nodes list at some level in menu hierarchy, 757*10465441SEvalZero collects nodes that may be displayed when viewing and editing that 758*10465441SEvalZero hierarchy level. Includes implicit menu nodes, i.e. the ones dependent 759*10465441SEvalZero on 'config' entry via 'if' statement which are internally represented 760*10465441SEvalZero as children of their dependency 761*10465441SEvalZero """ 762*10465441SEvalZero entries = [] 763*10465441SEvalZero n = start_node 764*10465441SEvalZero while n is not None: 765*10465441SEvalZero entries.append(ListEntry(self, n, indent)) 766*10465441SEvalZero # If node refers to a symbol (X) and has children, it is either 767*10465441SEvalZero # 'config' or 'menuconfig'. The children are items inside 'if X' 768*10465441SEvalZero # block that immediately follows 'config' or 'menuconfig' entry. 769*10465441SEvalZero # If it's a 'menuconfig' then corresponding MenuNode is shown as a 770*10465441SEvalZero # regular menu entry. But if it's a 'config', then its children need 771*10465441SEvalZero # to be shown in the same list with their texts indented 772*10465441SEvalZero if (n.list is not None 773*10465441SEvalZero and isinstance(n.item, kconfiglib.Symbol) 774*10465441SEvalZero and n.is_menuconfig == False): 775*10465441SEvalZero entries.extend( 776*10465441SEvalZero self._collect_list_entries(n.list, indent=indent + 1) 777*10465441SEvalZero ) 778*10465441SEvalZero n = n.next 779*10465441SEvalZero return entries 780*10465441SEvalZero 781*10465441SEvalZero def refresh_display(self, reset_selection=False): 782*10465441SEvalZero # Refresh list entries' attributes 783*10465441SEvalZero for e in self.all_entries: 784*10465441SEvalZero e.refresh() 785*10465441SEvalZero # Try to preserve selection upon refresh 786*10465441SEvalZero selected_entry = self._selected_entry 787*10465441SEvalZero # Also try to preserve listbox scroll offset 788*10465441SEvalZero # If not preserved, the see() method will make wanted item to appear 789*10465441SEvalZero # at the bottom of the list, even if previously it was in center 790*10465441SEvalZero scroll_offset = self.list.yview()[0] 791*10465441SEvalZero # Show only visible entries 792*10465441SEvalZero self.shown_entries = [e for e in self.all_entries if e.visible] 793*10465441SEvalZero # Refresh listbox contents 794*10465441SEvalZero self.list.delete(0, tk.END) 795*10465441SEvalZero self.list.insert(0, *self.shown_entries) 796*10465441SEvalZero if selected_entry and not reset_selection: 797*10465441SEvalZero # Restore scroll position 798*10465441SEvalZero self.list.yview_moveto(scroll_offset) 799*10465441SEvalZero # Activate previously selected node 800*10465441SEvalZero self._select_node(selected_entry.node) 801*10465441SEvalZero else: 802*10465441SEvalZero # Select the topmost entry 803*10465441SEvalZero self.list.activate(0) 804*10465441SEvalZero self._invert_colors(0) 805*10465441SEvalZero # Select ACTION_SELECT on each refresh (mimic C menuconfig) 806*10465441SEvalZero self.tk_selected_action.set(self.ACTION_SELECT) 807*10465441SEvalZero # Display current location in configuration tree 808*10465441SEvalZero pos = [] 809*10465441SEvalZero for n in self.node_stack + [self.node]: 810*10465441SEvalZero pos.append(n.prompt[0] if n.prompt else '[none]') 811*10465441SEvalZero self.label_position['text'] = u'# ' + u' -> '.join(pos) 812*10465441SEvalZero 813*10465441SEvalZero def show_node(self, node): 814*10465441SEvalZero self.node = node 815*10465441SEvalZero if node.list is not None: 816*10465441SEvalZero self.all_entries = self._collect_list_entries(node.list) 817*10465441SEvalZero else: 818*10465441SEvalZero self.all_entries = [] 819*10465441SEvalZero self.refresh_display(reset_selection=True) 820*10465441SEvalZero 821*10465441SEvalZero def show_submenu(self, node): 822*10465441SEvalZero self.node_stack.append(self.node) 823*10465441SEvalZero self.show_node(node) 824*10465441SEvalZero 825*10465441SEvalZero def show_parent(self): 826*10465441SEvalZero if self.node_stack: 827*10465441SEvalZero select_node = self.node 828*10465441SEvalZero parent_node = self.node_stack.pop() 829*10465441SEvalZero self.show_node(parent_node) 830*10465441SEvalZero # Restore previous selection 831*10465441SEvalZero self._select_node(select_node) 832*10465441SEvalZero self.refresh_display(reset_selection=False) 833*10465441SEvalZero 834*10465441SEvalZero def ask_for_string(self, ident=None, title='Enter string', value=None): 835*10465441SEvalZero """ 836*10465441SEvalZero Raises dialog with text entry widget and asks user to enter string 837*10465441SEvalZero 838*10465441SEvalZero Return: 839*10465441SEvalZero - str - user entered string 840*10465441SEvalZero - None - entry was cancelled 841*10465441SEvalZero """ 842*10465441SEvalZero text = 'Please enter a string value\n' \ 843*10465441SEvalZero 'User <Enter> key to accept the value\n' \ 844*10465441SEvalZero 'Use <Esc> key to cancel entry\n' 845*10465441SEvalZero d = EntryDialog(self.root, text, title, ident=ident, value=value) 846*10465441SEvalZero self.root.wait_window(d.dlg) 847*10465441SEvalZero self.list.focus_set() 848*10465441SEvalZero return d.value 849*10465441SEvalZero 850*10465441SEvalZero def ask_for_int(self, ident=None, title='Enter integer value', value=None, ranges=()): 851*10465441SEvalZero """ 852*10465441SEvalZero Raises dialog with text entry widget and asks user to enter decimal number 853*10465441SEvalZero Ranges should be iterable of tuples (start, end), 854*10465441SEvalZero where 'start' and 'end' specify allowed value range (inclusively) 855*10465441SEvalZero 856*10465441SEvalZero Return: 857*10465441SEvalZero - int - when valid number that falls within any one of specified ranges is entered 858*10465441SEvalZero - None - invalid number or entry was cancelled 859*10465441SEvalZero """ 860*10465441SEvalZero text = 'Please enter a decimal value. Fractions will not be accepted\n' \ 861*10465441SEvalZero 'User <Enter> key to accept the value\n' \ 862*10465441SEvalZero 'Use <Esc> key to cancel entry\n' 863*10465441SEvalZero d = EntryDialog(self.root, text, title, ident=ident, value=value) 864*10465441SEvalZero self.root.wait_window(d.dlg) 865*10465441SEvalZero self.list.focus_set() 866*10465441SEvalZero ivalue = None 867*10465441SEvalZero if d.value: 868*10465441SEvalZero try: 869*10465441SEvalZero ivalue = int(d.value) 870*10465441SEvalZero except ValueError: 871*10465441SEvalZero messagebox.showerror('Bad value', 'Entered value \'{}\' is not an integer'.format(d.value)) 872*10465441SEvalZero if ivalue is not None and ranges: 873*10465441SEvalZero allowed = False 874*10465441SEvalZero for start, end in ranges: 875*10465441SEvalZero allowed = allowed or start <= ivalue and ivalue <= end 876*10465441SEvalZero if not allowed: 877*10465441SEvalZero messagebox.showerror( 878*10465441SEvalZero 'Bad value', 879*10465441SEvalZero 'Entered value \'{:d}\' is out of range\n' 880*10465441SEvalZero 'Allowed:\n{}'.format( 881*10465441SEvalZero ivalue, 882*10465441SEvalZero '\n'.join([' {:d} - {:d}'.format(s,e) for s,e in ranges]) 883*10465441SEvalZero ) 884*10465441SEvalZero ) 885*10465441SEvalZero ivalue = None 886*10465441SEvalZero return ivalue 887*10465441SEvalZero 888*10465441SEvalZero def ask_for_hex(self, ident=None, title='Enter hexadecimal value', value=None, ranges=()): 889*10465441SEvalZero """ 890*10465441SEvalZero Raises dialog with text entry widget and asks user to enter decimal number 891*10465441SEvalZero Ranges should be iterable of tuples (start, end), 892*10465441SEvalZero where 'start' and 'end' specify allowed value range (inclusively) 893*10465441SEvalZero 894*10465441SEvalZero Return: 895*10465441SEvalZero - int - when valid number that falls within any one of specified ranges is entered 896*10465441SEvalZero - None - invalid number or entry was cancelled 897*10465441SEvalZero """ 898*10465441SEvalZero text = 'Please enter a hexadecimal value\n' \ 899*10465441SEvalZero 'User <Enter> key to accept the value\n' \ 900*10465441SEvalZero 'Use <Esc> key to cancel entry\n' 901*10465441SEvalZero d = EntryDialog(self.root, text, title, ident=ident, value=value) 902*10465441SEvalZero self.root.wait_window(d.dlg) 903*10465441SEvalZero self.list.focus_set() 904*10465441SEvalZero hvalue = None 905*10465441SEvalZero if d.value: 906*10465441SEvalZero try: 907*10465441SEvalZero hvalue = int(d.value, base=16) 908*10465441SEvalZero except ValueError: 909*10465441SEvalZero messagebox.showerror('Bad value', 'Entered value \'{}\' is not a hexadecimal value'.format(d.value)) 910*10465441SEvalZero if hvalue is not None and ranges: 911*10465441SEvalZero allowed = False 912*10465441SEvalZero for start, end in ranges: 913*10465441SEvalZero allowed = allowed or start <= hvalue and hvalue <= end 914*10465441SEvalZero if not allowed: 915*10465441SEvalZero messagebox.showerror( 916*10465441SEvalZero 'Bad value', 917*10465441SEvalZero 'Entered value \'0x{:x}\' is out of range\n' 918*10465441SEvalZero 'Allowed:\n{}'.format( 919*10465441SEvalZero hvalue, 920*10465441SEvalZero '\n'.join([' 0x{:x} - 0x{:x}'.format(s,e) for s,e in ranges]) 921*10465441SEvalZero ) 922*10465441SEvalZero ) 923*10465441SEvalZero hvalue = None 924*10465441SEvalZero return hvalue 925*10465441SEvalZero 926*10465441SEvalZero def show_text(self, text, title='Info'): 927*10465441SEvalZero """ 928*10465441SEvalZero Raises dialog with read-only text view that contains supplied text 929*10465441SEvalZero """ 930*10465441SEvalZero d = TextDialog(self.root, text, title) 931*10465441SEvalZero self.root.wait_window(d.dlg) 932*10465441SEvalZero self.list.focus_set() 933*10465441SEvalZero 934*10465441SEvalZero def mark_as_changed(self): 935*10465441SEvalZero """ 936*10465441SEvalZero Marks current config as having unsaved changes 937*10465441SEvalZero Should be called whenever config value is changed 938*10465441SEvalZero """ 939*10465441SEvalZero self.unsaved_changes = True 940*10465441SEvalZero self.update_status() 941*10465441SEvalZero 942*10465441SEvalZero def set_status_string(self, status): 943*10465441SEvalZero """ 944*10465441SEvalZero Sets status string displayed at the bottom of the window 945*10465441SEvalZero """ 946*10465441SEvalZero self.status_string = status 947*10465441SEvalZero self.update_status() 948*10465441SEvalZero 949*10465441SEvalZero def update_status(self): 950*10465441SEvalZero """ 951*10465441SEvalZero Updates status bar display 952*10465441SEvalZero Status bar displays: 953*10465441SEvalZero - unsaved status 954*10465441SEvalZero - current config path 955*10465441SEvalZero - status string (see set_status_string()) 956*10465441SEvalZero """ 957*10465441SEvalZero if self.__silent is True: 958*10465441SEvalZero return 959*10465441SEvalZero self.tk_status.set('{} [{}] {}'.format( 960*10465441SEvalZero '<UNSAVED>' if self.unsaved_changes else '', 961*10465441SEvalZero self.config_path if self.config_path else '', 962*10465441SEvalZero self.status_string 963*10465441SEvalZero )) 964*10465441SEvalZero 965*10465441SEvalZero def _check_is_visible(self, node): 966*10465441SEvalZero v = True 967*10465441SEvalZero v = v and node.prompt is not None 968*10465441SEvalZero # It should be enough to check if prompt expression is not false and 969*10465441SEvalZero # for menu nodes whether 'visible if' is not false 970*10465441SEvalZero v = v and kconfiglib.expr_value(node.prompt[1]) > 0 971*10465441SEvalZero if node.item == kconfiglib.MENU: 972*10465441SEvalZero v = v and kconfiglib.expr_value(node.visibility) > 0 973*10465441SEvalZero # If node references Symbol, then we also account for symbol visibility 974*10465441SEvalZero # TODO: need to re-think whether this is needed 975*10465441SEvalZero if isinstance(node.item, kconfiglib.Symbol): 976*10465441SEvalZero if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE): 977*10465441SEvalZero v = v and len(node.item.assignable) > 0 978*10465441SEvalZero else: 979*10465441SEvalZero v = v and node.item.visibility > 0 980*10465441SEvalZero return v 981*10465441SEvalZero 982*10465441SEvalZero def config_is_changed(self): 983*10465441SEvalZero is_changed = False 984*10465441SEvalZero node = self.kconfig.top_node.list 985*10465441SEvalZero if not node: 986*10465441SEvalZero # Empty configuration 987*10465441SEvalZero return is_changed 988*10465441SEvalZero 989*10465441SEvalZero while 1: 990*10465441SEvalZero item = node.item 991*10465441SEvalZero if isinstance(item, kconfiglib.Symbol) and item.user_value is None and self._check_is_visible(node): 992*10465441SEvalZero is_changed = True 993*10465441SEvalZero print("Config \"# {}\" has changed, need save config file\n".format(node.prompt[0])) 994*10465441SEvalZero break; 995*10465441SEvalZero 996*10465441SEvalZero # Iterative tree walk using parent pointers 997*10465441SEvalZero 998*10465441SEvalZero if node.list: 999*10465441SEvalZero node = node.list 1000*10465441SEvalZero elif node.next: 1001*10465441SEvalZero node = node.next 1002*10465441SEvalZero else: 1003*10465441SEvalZero while node.parent: 1004*10465441SEvalZero node = node.parent 1005*10465441SEvalZero if node.next: 1006*10465441SEvalZero node = node.next 1007*10465441SEvalZero break 1008*10465441SEvalZero else: 1009*10465441SEvalZero break 1010*10465441SEvalZero return is_changed 1011*10465441SEvalZero 1012*10465441SEvalZero def prevent_losing_changes(self): 1013*10465441SEvalZero """ 1014*10465441SEvalZero Checks if there are unsaved changes and asks user to save or discard them 1015*10465441SEvalZero This routine should be called whenever current config is going to be discarded 1016*10465441SEvalZero 1017*10465441SEvalZero Raises the usual 'Yes', 'No', 'Cancel' prompt. 1018*10465441SEvalZero 1019*10465441SEvalZero Return: 1020*10465441SEvalZero - True: caller may safely drop current config state 1021*10465441SEvalZero - False: user needs to continue work on current config ('Cancel' pressed or saving failed) 1022*10465441SEvalZero """ 1023*10465441SEvalZero if self.config_is_changed() == True: 1024*10465441SEvalZero self.mark_as_changed() 1025*10465441SEvalZero if not self.unsaved_changes: 1026*10465441SEvalZero return True 1027*10465441SEvalZero 1028*10465441SEvalZero if self.__silent: 1029*10465441SEvalZero saved = self.save_config() 1030*10465441SEvalZero return saved 1031*10465441SEvalZero res = messagebox.askyesnocancel( 1032*10465441SEvalZero parent=self.root, 1033*10465441SEvalZero title='Unsaved changes', 1034*10465441SEvalZero message='Config has unsaved changes. Do you want to save them?' 1035*10465441SEvalZero ) 1036*10465441SEvalZero if res is None: 1037*10465441SEvalZero return False 1038*10465441SEvalZero elif res is False: 1039*10465441SEvalZero return True 1040*10465441SEvalZero # Otherwise attempt to save config and succeed only if config has been saved successfully 1041*10465441SEvalZero saved = self.save_config() 1042*10465441SEvalZero return saved 1043*10465441SEvalZero 1044*10465441SEvalZero def open_config(self, path=None): 1045*10465441SEvalZero if path is None: 1046*10465441SEvalZero # Create open dialog. Either existing file is selected or no file is selected as a result 1047*10465441SEvalZero path = filedialog.askopenfilename( 1048*10465441SEvalZero parent=self.root, 1049*10465441SEvalZero title='Open config..', 1050*10465441SEvalZero initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(), 1051*10465441SEvalZero filetypes=(('.config files', '*.config'), ('All files', '*.*')) 1052*10465441SEvalZero ) 1053*10465441SEvalZero if not path or not os.path.isfile(path): 1054*10465441SEvalZero return False 1055*10465441SEvalZero path = os.path.abspath(path) 1056*10465441SEvalZero print('Loading config: \'{}\''.format(path)) 1057*10465441SEvalZero # Try to open given path 1058*10465441SEvalZero # If path does not exist, we still set current config path to it but don't load anything 1059*10465441SEvalZero self.unsaved_changes = False 1060*10465441SEvalZero self.config_path = path 1061*10465441SEvalZero if not os.path.exists(path): 1062*10465441SEvalZero self.set_status_string('New config') 1063*10465441SEvalZero self.mark_as_changed() 1064*10465441SEvalZero return True 1065*10465441SEvalZero # Load config and set status accordingly 1066*10465441SEvalZero try: 1067*10465441SEvalZero self.kconfig.load_config(path) 1068*10465441SEvalZero except IOError as e: 1069*10465441SEvalZero self.set_status_string('Failed to load: \'{}\''.format(path)) 1070*10465441SEvalZero if not self.__silent: 1071*10465441SEvalZero self.refresh_display() 1072*10465441SEvalZero print('Failed to load config \'{}\': {}'.format(path, e)) 1073*10465441SEvalZero return False 1074*10465441SEvalZero self.set_status_string('Opened config') 1075*10465441SEvalZero if not self.__silent: 1076*10465441SEvalZero self.refresh_display() 1077*10465441SEvalZero return True 1078*10465441SEvalZero 1079*10465441SEvalZero def save_config(self, force_file_dialog=False): 1080*10465441SEvalZero path = self.config_path 1081*10465441SEvalZero if path is None or force_file_dialog: 1082*10465441SEvalZero path = filedialog.asksaveasfilename( 1083*10465441SEvalZero parent=self.root, 1084*10465441SEvalZero title='Save config as..', 1085*10465441SEvalZero initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(), 1086*10465441SEvalZero initialfile=os.path.basename(self.config_path) if self.config_path else None, 1087*10465441SEvalZero defaultextension='.config', 1088*10465441SEvalZero filetypes=(('.config files', '*.config'), ('All files', '*.*')) 1089*10465441SEvalZero ) 1090*10465441SEvalZero if not path: 1091*10465441SEvalZero return False 1092*10465441SEvalZero path = os.path.abspath(path) 1093*10465441SEvalZero print('Saving config: \'{}\''.format(path)) 1094*10465441SEvalZero # Try to save config to selected path 1095*10465441SEvalZero try: 1096*10465441SEvalZero self.kconfig.write_config(path, header="#\n# Automatically generated file; DO NOT EDIT.\n") 1097*10465441SEvalZero self.unsaved_changes = False 1098*10465441SEvalZero self.config_path = path 1099*10465441SEvalZero self.set_status_string('Saved config') 1100*10465441SEvalZero except IOError as e: 1101*10465441SEvalZero self.set_status_string('Failed to save: \'{}\''.format(path)) 1102*10465441SEvalZero print('Save failed: {}'.format(e), file=sys.stderr) 1103*10465441SEvalZero return False 1104*10465441SEvalZero return True 1105*10465441SEvalZero 1106*10465441SEvalZero 1107*10465441SEvalZerodef _center_window(root, window): 1108*10465441SEvalZero # type: (tk.Tk, tk.Toplevel) -> None 1109*10465441SEvalZero """ 1110*10465441SEvalZero Attempts to center window on screen 1111*10465441SEvalZero """ 1112*10465441SEvalZero root.update_idletasks() 1113*10465441SEvalZero # root.eval('tk::PlaceWindow {!s} center'.format( 1114*10465441SEvalZero # window.winfo_pathname(window.winfo_id()) 1115*10465441SEvalZero # )) 1116*10465441SEvalZero w = window.winfo_width() 1117*10465441SEvalZero h = window.winfo_height() 1118*10465441SEvalZero ws = window.winfo_screenwidth() 1119*10465441SEvalZero hs = window.winfo_screenheight() 1120*10465441SEvalZero x = (ws / 2) - (w / 2) 1121*10465441SEvalZero y = (hs / 2) - (h / 2) 1122*10465441SEvalZero window.geometry('+{:d}+{:d}'.format(int(x), int(y))) 1123*10465441SEvalZero window.lift() 1124*10465441SEvalZero window.focus_force() 1125*10465441SEvalZero 1126*10465441SEvalZero 1127*10465441SEvalZerodef _center_window_above_parent(root, window): 1128*10465441SEvalZero # type: (tk.Tk, tk.Toplevel) -> None 1129*10465441SEvalZero """ 1130*10465441SEvalZero Attempts to center window above its parent window 1131*10465441SEvalZero """ 1132*10465441SEvalZero # root.eval('tk::PlaceWindow {!s} center'.format( 1133*10465441SEvalZero # window.winfo_pathname(window.winfo_id()) 1134*10465441SEvalZero # )) 1135*10465441SEvalZero root.update_idletasks() 1136*10465441SEvalZero parent = window.master 1137*10465441SEvalZero w = window.winfo_width() 1138*10465441SEvalZero h = window.winfo_height() 1139*10465441SEvalZero px = parent.winfo_rootx() 1140*10465441SEvalZero py = parent.winfo_rooty() 1141*10465441SEvalZero pw = parent.winfo_width() 1142*10465441SEvalZero ph = parent.winfo_height() 1143*10465441SEvalZero x = px + (pw / 2) - (w / 2) 1144*10465441SEvalZero y = py + (ph / 2) - (h / 2) 1145*10465441SEvalZero window.geometry('+{:d}+{:d}'.format(int(x), int(y))) 1146*10465441SEvalZero window.lift() 1147*10465441SEvalZero window.focus_force() 1148*10465441SEvalZero 1149*10465441SEvalZero 1150*10465441SEvalZerodef main(argv=None): 1151*10465441SEvalZero if argv is None: 1152*10465441SEvalZero argv = sys.argv[1:] 1153*10465441SEvalZero # Instantiate cmd options parser 1154*10465441SEvalZero parser = argparse.ArgumentParser( 1155*10465441SEvalZero description='Interactive Kconfig configuration editor' 1156*10465441SEvalZero ) 1157*10465441SEvalZero parser.add_argument( 1158*10465441SEvalZero '--kconfig', 1159*10465441SEvalZero metavar='FILE', 1160*10465441SEvalZero type=str, 1161*10465441SEvalZero default='Kconfig', 1162*10465441SEvalZero help='path to root Kconfig file' 1163*10465441SEvalZero ) 1164*10465441SEvalZero parser.add_argument( 1165*10465441SEvalZero '--config', 1166*10465441SEvalZero metavar='FILE', 1167*10465441SEvalZero type=str, 1168*10465441SEvalZero help='path to .config file to load' 1169*10465441SEvalZero ) 1170*10465441SEvalZero if "--silent" in argv: 1171*10465441SEvalZero parser.add_argument( 1172*10465441SEvalZero '--silent', 1173*10465441SEvalZero dest = '_silent_', 1174*10465441SEvalZero type=str, 1175*10465441SEvalZero help='silent mode, not show window' 1176*10465441SEvalZero ) 1177*10465441SEvalZero args = parser.parse_args(argv) 1178*10465441SEvalZero kconfig_path = args.kconfig 1179*10465441SEvalZero config_path = args.config 1180*10465441SEvalZero # Verify that Kconfig file exists 1181*10465441SEvalZero if not os.path.isfile(kconfig_path): 1182*10465441SEvalZero raise RuntimeError('\'{}\': no such file'.format(kconfig_path)) 1183*10465441SEvalZero 1184*10465441SEvalZero # Parse Kconfig files 1185*10465441SEvalZero kconf = kconfiglib.Kconfig(filename=kconfig_path) 1186*10465441SEvalZero 1187*10465441SEvalZero if "--silent" not in argv: 1188*10465441SEvalZero print("In normal mode. Will show menuconfig window.") 1189*10465441SEvalZero mc = MenuConfig(kconf) 1190*10465441SEvalZero # If config file was specified, load it 1191*10465441SEvalZero if config_path: 1192*10465441SEvalZero mc.open_config(config_path) 1193*10465441SEvalZero 1194*10465441SEvalZero print("Enter mainloop. Waiting...") 1195*10465441SEvalZero tk.mainloop() 1196*10465441SEvalZero else: 1197*10465441SEvalZero print("In silent mode. Don`t show menuconfig window.") 1198*10465441SEvalZero mc = MenuConfig(kconf, True) 1199*10465441SEvalZero # If config file was specified, load it 1200*10465441SEvalZero if config_path: 1201*10465441SEvalZero mc.open_config(config_path) 1202*10465441SEvalZero mc._close_window() 1203*10465441SEvalZero 1204*10465441SEvalZero 1205*10465441SEvalZeroif __name__ == '__main__': 1206*10465441SEvalZero main() 1207