123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206 |
- # SPDX-License-Identifier: ISC
- # -*- coding: utf-8 -*-
- """
- Overview
- ========
- pymenuconfig is a small and simple frontend to Kconfiglib that's written
- entirely in Python using Tkinter as its GUI toolkit.
- Motivation
- ==========
- Kconfig is a nice and powerful framework for build-time configuration and lots
- of projects already benefit from using it. Kconfiglib allows to utilize power of
- Kconfig by using scripts written in pure Python, without requiring one to build
- Linux kernel tools written in C (this can be quite tedious on anything that's
- not *nix). The aim of this project is to implement simple and small Kconfiglib
- GUI frontend that runs on as much systems as possible.
- Tkinter GUI toolkit is a natural choice if portability is considered, as it's
- a part of Python standard library and is available virtually in every CPython
- installation.
- User interface
- ==============
- I've tried to replicate look and fill of Linux kernel 'menuconfig' tool that
- many users are used to, including keyboard-oriented control and textual
- representation of menus with fixed-width font.
- Usage
- =====
- The pymenuconfig module is executable and parses command-line args, so the
- most simple way to run menuconfig is to execute script directly:
- python pymenuconfig.py --kconfig Kconfig
- As with most command-line tools list of options can be obtained with '--help':
- python pymenuconfig.py --help
- If installed with setuptools, one can run it like this:
- python -m pymenuconfig --kconfig Kconfig
- In case you're making a wrapper around menuconfig, you can either call main():
- import pymenuconfig
- pymenuconfig.main(['--kconfig', 'Kconfig'])
- Or import MenuConfig class, instantiate it and manually run Tkinter's mainloop:
- import tkinter
- import kconfiglib
- from pymenuconfig import MenuConfig
- kconfig = kconfiglib.Kconfig()
- mconf = MenuConfig(kconfig)
- tkinter.mainloop()
- """
- from __future__ import print_function
- import os
- import sys
- import argparse
- import kconfiglib
- # Tk is imported differently depending on python major version
- if sys.version_info[0] < 3:
- import Tkinter as tk
- import tkFont as font
- import tkFileDialog as filedialog
- import tkMessageBox as messagebox
- else:
- import tkinter as tk
- from tkinter import font
- from tkinter import filedialog
- from tkinter import messagebox
- class ListEntry(object):
- """
- Represents visible menu node and holds all information related to displaying
- menu node in a Listbox.
- Instances of this class also handle all interaction with main window.
- A node is displayed as a single line of text:
- PREFIX INDENT BODY POSTFIX
- - The PREFIX is always 3 characters or more and can take following values:
- ' ' comment, menu, bool choice, etc.
- Inside menus:
- '< >' bool symbol has value 'n'
- '<*>' bool symbol has value 'y'
- '[ ]' tristate symbol has value 'n'
- '[M]' tristate symbol has value 'm'
- '[*]' tristate symbol has value 'y'
- '- -' symbol has value 'n' that's not editable
- '-M-' symbol has value 'm' that's not editable
- '-*-' symbol has value 'y' that's not editable
- '(M)' tristate choice has value 'm'
- '(*)' tristate choice has value 'y'
- '(some value)' value of non-bool/tristate symbols
- Inside choices:
- '( )' symbol has value 'n'
- '(M)' symbol has value 'm'
- '(*)' symbol has value 'y'
- - INDENT is a sequence of space characters. It's used in implicit menus, and
- adds 2 spaces for each nesting level
- - BODY is a menu node prompt. '***' is added if node is a comment
- - POSTFIX adds '(NEW)', '--->' and selected choice symbol where applicable
- Attributes:
- node:
- MenuNode instance this ListEntry is created for.
- visible:
- Whether entry should be shown in main window.
- text:
- String to display in a main window's Listbox.
- refresh():
- Updates .visible and .text attribute values.
- set_tristate_value():
- Set value for bool/tristate symbols, value should be one of 0,1,2 or None.
- Usually it's called when user presses 'y', 'n', 'm' key.
- set_str_value():
- Set value for non-bool/tristate symbols, value is a string. Usually called
- with a value returned by one of MenuConfig.ask_for_* methods.
- toggle():
- Toggle bool/tristate symbol value. Called when '<Space>' key is pressed in
- a main window. Also selects choice value.
- select():
- Called when '<Return>' key is pressed in a main window with 'SELECT'
- action selected. Displays submenu, choice selection menu, or just selects
- choice value. For non-bool/tristate symbols asks MenuConfig window to
- handle value input via one of MenuConfig.ask_for_* methods.
- show_help():
- Called when '<Return>' key is pressed in a main window with 'HELP' action
- selected. Prepares text help and calls MenuConfig.show_text() to display
- text window.
- """
- # How to display value of BOOL and TRISTATE symbols
- TRI_TO_DISPLAY = {
- 0: ' ',
- 1: 'M',
- 2: '*'
- }
- def __init__(self, mconf, node, indent):
- self.indent = indent
- self.node = node
- self.menuconfig = mconf
- self.visible = False
- self.text = None
- def __str__(self):
- return self.text
- def _is_visible(self):
- node = self.node
- v = True
- v = v and node.prompt is not None
- # It should be enough to check if prompt expression is not false and
- # for menu nodes whether 'visible if' is not false
- v = v and kconfiglib.expr_value(node.prompt[1]) > 0
- if node.item == kconfiglib.MENU:
- v = v and kconfiglib.expr_value(node.visibility) > 0
- # If node references Symbol, then we also account for symbol visibility
- # TODO: need to re-think whether this is needed
- if isinstance(node.item, kconfiglib.Symbol):
- if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
- v = v and len(node.item.assignable) > 0
- else:
- v = v and node.item.visibility > 0
- return v
- def _get_text(self):
- """
- Compute textual representation of menu node (a line in ListView)
- """
- node = self.node
- item = node.item
- # Determine prefix
- prefix = ' '
- if (isinstance(item, kconfiglib.Symbol) and item.choice is None or
- isinstance(item, kconfiglib.Choice) and item.type is kconfiglib.TRISTATE):
- # The node is for either a symbol outside of choice statement
- # or a tristate choice
- if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
- value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
- if len(item.assignable) > 1:
- # Symbol is editable
- if 1 in item.assignable:
- prefix = '<{}>'.format(value)
- else:
- prefix = '[{}]'.format(value)
- else:
- # Symbol is not editable
- prefix = '-{}-'.format(value)
- else:
- prefix = '({})'.format(item.str_value)
- elif isinstance(item, kconfiglib.Symbol) and item.choice is not None:
- # The node is for symbol inside choice statement
- if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
- value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
- if len(item.assignable) > 0:
- # Symbol is editable
- prefix = '({})'.format(value)
- else:
- # Symbol is not editable
- prefix = '-{}-'.format(value)
- else:
- prefix = '({})'.format(item.str_value)
- # Prefix should be at least 3 chars long
- if len(prefix) < 3:
- prefix += ' ' * (3 - len(prefix))
- # Body
- body = ''
- if node.prompt is not None:
- if item is kconfiglib.COMMENT:
- body = '*** {} ***'.format(node.prompt[0])
- else:
- body = node.prompt[0]
- # Suffix
- is_menu = False
- is_new = False
- if (item is kconfiglib.MENU
- or isinstance(item, kconfiglib.Symbol) and node.is_menuconfig
- or isinstance(item, kconfiglib.Choice)):
- is_menu = True
- if isinstance(item, kconfiglib.Symbol) and item.user_value is None:
- is_new = True
- # For symbol inside choice that has 'y' value, '(NEW)' is not displayed
- if (isinstance(item, kconfiglib.Symbol)
- and item.choice and item.choice.tri_value == 2):
- is_new = False
- # Choice selection - displayed only for choices which have 'y' value
- choice_selection = None
- if isinstance(item, kconfiglib.Choice) and node.item.str_value == 'y':
- choice_selection = ''
- if item.selection is not None:
- sym = item.selection
- if sym.nodes and sym.nodes[0].prompt is not None:
- choice_selection = sym.nodes[0].prompt[0]
- text = ' {prefix} {indent}{body}{choice}{new}{menu}'.format(
- prefix=prefix,
- indent=' ' * self.indent,
- body=body,
- choice='' if choice_selection is None else ' ({})'.format(
- choice_selection
- ),
- new=' (NEW)' if is_new else '',
- menu=' --->' if is_menu else ''
- )
- return text
- def refresh(self):
- self.visible = self._is_visible()
- self.text = self._get_text()
- def set_tristate_value(self, value):
- """
- Call to change value of BOOL, TRISTATE symbols
- It's preferred to use this instead of item.set_value as it handles
- all necessary interaction with MenuConfig window when symbol value
- changes
- None value is accepted but ignored
- """
- item = self.node.item
- if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
- and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)
- and value is not None):
- if value in item.assignable:
- item.set_value(value)
- elif value == 2 and 1 in item.assignable:
- print(
- 'Symbol {} value is limited to \'m\'. Setting value \'m\' instead of \'y\''.format(item.name),
- file=sys.stderr
- )
- item.set_value(1)
- self.menuconfig.mark_as_changed()
- self.menuconfig.refresh_display()
- def set_str_value(self, value):
- """
- Call to change value of HEX, INT, STRING symbols
- It's preferred to use this instead of item.set_value as it handles
- all necessary interaction with MenuConfig window when symbol value
- changes
- None value is accepted but ignored
- """
- item = self.node.item
- if (isinstance(item, kconfiglib.Symbol)
- and item.type in (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)
- and value is not None):
- item.set_value(value)
- self.menuconfig.mark_as_changed()
- self.menuconfig.refresh_display()
- def toggle(self):
- """
- Called when <space> key is pressed
- """
- item = self.node.item
- if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
- and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)):
- value = item.tri_value
- # Find next value in Symbol/Choice.assignable, or use assignable[0]
- try:
- it = iter(item.assignable)
- while value != next(it):
- pass
- self.set_tristate_value(next(it))
- except StopIteration:
- self.set_tristate_value(item.assignable[0])
- def select(self):
- """
- Called when <Return> key is pressed and SELECT action is selected
- """
- item = self.node.item
- # - Menu: dive into submenu
- # - INT, HEX, STRING symbol: raise prompt to enter symbol value
- # - BOOL, TRISTATE symbol inside 'y'-valued Choice: set 'y' value
- if (item is kconfiglib.MENU
- or isinstance(item, kconfiglib.Symbol) and self.node.is_menuconfig
- or isinstance(item, kconfiglib.Choice)):
- # Dive into submenu
- self.menuconfig.show_submenu(self.node)
- elif (isinstance(item, kconfiglib.Symbol) and item.type in
- (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)):
- # Raise prompt to enter symbol value
- ident = self.node.prompt[0] if self.node.prompt is not None else None
- title = 'Symbol: {}'.format(item.name)
- if item.type is kconfiglib.INT:
- # Find enabled ranges
- ranges = [
- (int(start.str_value), int(end.str_value))
- for start, end, expr in item.ranges
- if kconfiglib.expr_value(expr) > 0
- ]
- # Raise prompt
- self.set_str_value(str(self.menuconfig.ask_for_int(
- ident=ident,
- title=title,
- value=item.str_value,
- ranges=ranges
- )))
- elif item.type is kconfiglib.HEX:
- # Find enabled ranges
- ranges = [
- (int(start.str_value, base=16), int(end.str_value, base=16))
- for start, end, expr in item.ranges
- if kconfiglib.expr_value(expr) > 0
- ]
- # Raise prompt
- self.set_str_value(hex(self.menuconfig.ask_for_hex(
- ident=ident,
- title=title,
- value=item.str_value,
- ranges=ranges
- )))
- elif item.type is kconfiglib.STRING:
- # Raise prompt
- self.set_str_value(self.menuconfig.ask_for_string(
- ident=ident,
- title=title,
- value=item.str_value
- ))
- elif (isinstance(item, kconfiglib.Symbol)
- and item.choice is not None and item.choice.tri_value == 2):
- # Symbol inside choice -> set symbol value to 'y'
- self.set_tristate_value(2)
- def show_help(self):
- node = self.node
- item = self.node.item
- if isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)):
- title = 'Help for symbol: {}'.format(item.name)
- if node.help:
- help = node.help
- else:
- help = 'There is no help available for this option.\n'
- lines = []
- lines.append(help)
- lines.append(
- 'Symbol: {} [={}]'.format(
- item.name if item.name else '<UNNAMED>', item.str_value
- )
- )
- lines.append('Type : {}'.format(kconfiglib.TYPE_TO_STR[item.type]))
- for n in item.nodes:
- lines.append('Prompt: {}'.format(n.prompt[0] if n.prompt else '<EMPTY>'))
- lines.append(' Defined at {}:{}'.format(n.filename, n.linenr))
- lines.append(' Depends on: {}'.format(kconfiglib.expr_str(n.dep)))
- text = '\n'.join(lines)
- else:
- title = 'Help'
- text = 'Help not available for this menu node.\n'
- self.menuconfig.show_text(text, title)
- self.menuconfig.refresh_display()
- class EntryDialog(object):
- """
- Creates modal dialog (top-level Tk window) with labels, entry box and two
- buttons: OK and CANCEL.
- """
- def __init__(self, master, text, title, ident=None, value=None):
- self.master = master
- dlg = self.dlg = tk.Toplevel(master)
- self.dlg.withdraw() #hiden window
- dlg.title(title)
- # Identifier label
- if ident is not None:
- self.label_id = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
- self.label_id['font'] = font.nametofont('TkFixedFont')
- self.label_id['text'] = '# {}'.format(ident)
- self.label_id.pack(fill=tk.X, padx=2, pady=2)
- # Label
- self.label = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
- self.label['font'] = font.nametofont('TkFixedFont')
- self.label['text'] = text
- self.label.pack(fill=tk.X, padx=10, pady=4)
- # Entry box
- self.entry = tk.Entry(dlg)
- self.entry['font'] = font.nametofont('TkFixedFont')
- self.entry.pack(fill=tk.X, padx=2, pady=2)
- # Frame for buttons
- self.frame = tk.Frame(dlg)
- self.frame.pack(padx=2, pady=2)
- # Button
- self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
- self.btn_accept['font'] = font.nametofont('TkFixedFont')
- self.btn_accept.pack(side=tk.LEFT, padx=2)
- self.btn_cancel = tk.Button(self.frame, text='< Cancel >', command=self.cancel)
- self.btn_cancel['font'] = font.nametofont('TkFixedFont')
- self.btn_cancel.pack(side=tk.LEFT, padx=2)
- # Bind Enter and Esc keys
- self.dlg.bind('<Return>', self.accept)
- self.dlg.bind('<Escape>', self.cancel)
- # Dialog is resizable only by width
- self.dlg.resizable(1, 0)
- # Set supplied value (if any)
- if value is not None:
- self.entry.insert(0, value)
- self.entry.selection_range(0, tk.END)
- # By default returned value is None. To caller this means that entry
- # process was cancelled
- self.value = None
- # Modal dialog
- dlg.transient(master)
- dlg.grab_set()
- # Center dialog window
- _center_window_above_parent(master, dlg)
- self.dlg.deiconify() # show window
- # Focus entry field
- self.entry.focus_set()
- def accept(self, ev=None):
- self.value = self.entry.get()
- self.dlg.destroy()
- def cancel(self, ev=None):
- self.dlg.destroy()
- class TextDialog(object):
- def __init__(self, master, text, title):
- self.master = master
- dlg = self.dlg = tk.Toplevel(master)
- self.dlg.withdraw() #hiden window
- dlg.title(title)
- dlg.minsize(600,400)
- # Text
- self.text = tk.Text(dlg, height=1)
- self.text['font'] = font.nametofont('TkFixedFont')
- self.text.insert(tk.END, text)
- # Make text read-only
- self.text['state'] = tk.DISABLED
- self.text.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
- # Frame for buttons
- self.frame = tk.Frame(dlg)
- self.frame.pack(padx=2, pady=2)
- # Button
- self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
- self.btn_accept['font'] = font.nametofont('TkFixedFont')
- self.btn_accept.pack(side=tk.LEFT, padx=2)
- # Bind Enter and Esc keys
- self.dlg.bind('<Return>', self.accept)
- self.dlg.bind('<Escape>', self.cancel)
- # Modal dialog
- dlg.transient(master)
- dlg.grab_set()
- # Center dialog window
- _center_window_above_parent(master, dlg)
- self.dlg.deiconify() # show window
- # Focus entry field
- self.text.focus_set()
- def accept(self, ev=None):
- self.dlg.destroy()
- def cancel(self, ev=None):
- self.dlg.destroy()
- class MenuConfig(object):
- (
- ACTION_SELECT,
- ACTION_EXIT,
- ACTION_HELP,
- ACTION_LOAD,
- ACTION_SAVE,
- ACTION_SAVE_AS
- ) = range(6)
- ACTIONS = (
- ('Select', ACTION_SELECT),
- ('Exit', ACTION_EXIT),
- ('Help', ACTION_HELP),
- ('Load', ACTION_LOAD),
- ('Save', ACTION_SAVE),
- ('Save as', ACTION_SAVE_AS),
- )
- def __init__(self, kconfig, __silent=None):
- self.kconfig = kconfig
- self.__silent = __silent
- if self.__silent is True:
- return
- # Instantiate Tk widgets
- self.root = tk.Tk()
- self.root.withdraw() #hiden window
- dlg = self.root
- # Window title
- dlg.title('pymenuconfig')
- # Some empirical window size
- dlg.minsize(500, 300)
- dlg.geometry('800x600')
- # Label that shows position in menu tree
- self.label_position = tk.Label(
- dlg,
- anchor=tk.W,
- justify=tk.LEFT,
- font=font.nametofont('TkFixedFont')
- )
- self.label_position.pack(fill=tk.X, padx=2)
- # 'Tip' frame and text
- self.frame_tip = tk.LabelFrame(
- dlg,
- text='Tip'
- )
- self.label_tip = tk.Label(
- self.frame_tip,
- anchor=tk.W,
- justify=tk.LEFT,
- font=font.nametofont('TkFixedFont')
- )
- self.label_tip['text'] = '\n'.join([
- 'Arrow keys navigate the menu. <Enter> performs selected operation (set of buttons at the bottom)',
- 'Pressing <Y> includes, <N> excludes, <M> modularizes features',
- 'Press <Esc> to go one level up. Press <Esc> at top level to exit',
- 'Legend: [*] built-in [ ] excluded <M> module < > module capable'
- ])
- self.label_tip.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
- self.frame_tip.pack(fill=tk.X, padx=2)
- # Main ListBox where all the magic happens
- self.list = tk.Listbox(
- dlg,
- selectmode=tk.SINGLE,
- activestyle=tk.NONE,
- font=font.nametofont('TkFixedFont'),
- height=1,
- )
- self.list['foreground'] = 'Blue'
- self.list['background'] = 'Gray95'
- # Make selection invisible
- self.list['selectbackground'] = self.list['background']
- self.list['selectforeground'] = self.list['foreground']
- self.list.pack(fill=tk.BOTH, expand=1, padx=20, ipadx=2)
- # Frame with radio buttons
- self.frame_radio = tk.Frame(dlg)
- self.radio_buttons = []
- self.tk_selected_action = tk.IntVar()
- for text, value in MenuConfig.ACTIONS:
- btn = tk.Radiobutton(
- self.frame_radio,
- variable=self.tk_selected_action,
- value=value
- )
- btn['text'] = '< {} >'.format(text)
- btn['font'] = font.nametofont('TkFixedFont')
- btn['indicatoron'] = 0
- btn.pack(side=tk.LEFT)
- self.radio_buttons.append(btn)
- self.frame_radio.pack(anchor=tk.CENTER, pady=4)
- # Label with status information
- self.tk_status = tk.StringVar()
- self.label_status = tk.Label(
- dlg,
- textvariable=self.tk_status,
- anchor=tk.W,
- justify=tk.LEFT,
- font=font.nametofont('TkFixedFont')
- )
- self.label_status.pack(fill=tk.X, padx=4, pady=4)
- # Center window
- _center_window(self.root, dlg)
- self.root.deiconify() # show window
- # Disable keyboard focus on all widgets ...
- self._set_option_to_all_children(dlg, 'takefocus', 0)
- # ... except for main ListBox
- self.list['takefocus'] = 1
- self.list.focus_set()
- # Bind keys
- dlg.bind('<Escape>', self.handle_keypress)
- dlg.bind('<space>', self.handle_keypress)
- dlg.bind('<Return>', self.handle_keypress)
- dlg.bind('<Right>', self.handle_keypress)
- dlg.bind('<Left>', self.handle_keypress)
- dlg.bind('<Up>', self.handle_keypress)
- dlg.bind('<Down>', self.handle_keypress)
- dlg.bind('n', self.handle_keypress)
- dlg.bind('m', self.handle_keypress)
- dlg.bind('y', self.handle_keypress)
- # Register callback that's called when window closes
- dlg.wm_protocol('WM_DELETE_WINDOW', self._close_window)
- # Init fields
- self.node = None
- self.node_stack = []
- self.all_entries = []
- self.shown_entries = []
- self.config_path = None
- self.unsaved_changes = False
- self.status_string = 'NEW CONFIG'
- self.update_status()
- # Display first child of top level node (the top level node is 'mainmenu')
- self.show_node(self.kconfig.top_node)
- def _set_option_to_all_children(self, widget, option, value):
- widget[option] = value
- for n,c in widget.children.items():
- self._set_option_to_all_children(c, option, value)
- def _invert_colors(self, idx):
- self.list.itemconfig(idx, {'bg' : self.list['foreground']})
- self.list.itemconfig(idx, {'fg' : self.list['background']})
- @property
- def _selected_entry(self):
- # type: (...) -> ListEntry
- active_idx = self.list.index(tk.ACTIVE)
- if active_idx >= 0 and active_idx < len(self.shown_entries):
- return self.shown_entries[active_idx]
- return None
- def _select_node(self, node):
- # type: (kconfiglib.MenuNode) -> None
- """
- Attempts to select entry that corresponds to given MenuNode in main listbox
- """
- idx = None
- for i, e in enumerate(self.shown_entries):
- if e.node is node:
- idx = i
- break
- if idx is not None:
- self.list.activate(idx)
- self.list.see(idx)
- self._invert_colors(idx)
- def handle_keypress(self, ev):
- keysym = ev.keysym
- if keysym == 'Left':
- self._select_action(prev=True)
- elif keysym == 'Right':
- self._select_action(prev=False)
- elif keysym == 'Up':
- self.refresh_display(reset_selection=False)
- elif keysym == 'Down':
- self.refresh_display(reset_selection=False)
- elif keysym == 'space':
- self._selected_entry.toggle()
- elif keysym in ('n', 'm', 'y'):
- self._selected_entry.set_tristate_value(kconfiglib.STR_TO_TRI[keysym])
- elif keysym == 'Return':
- action = self.tk_selected_action.get()
- if action == self.ACTION_SELECT:
- self._selected_entry.select()
- elif action == self.ACTION_EXIT:
- self._action_exit()
- elif action == self.ACTION_HELP:
- self._selected_entry.show_help()
- elif action == self.ACTION_LOAD:
- if self.prevent_losing_changes():
- self.open_config()
- elif action == self.ACTION_SAVE:
- self.save_config()
- elif action == self.ACTION_SAVE_AS:
- self.save_config(force_file_dialog=True)
- elif keysym == 'Escape':
- self._action_exit()
- pass
- def _close_window(self):
- if self.prevent_losing_changes():
- print('Exiting..')
- if self.__silent is True:
- return
- self.root.destroy()
- def _action_exit(self):
- if self.node_stack:
- self.show_parent()
- else:
- self._close_window()
- def _select_action(self, prev=False):
- # Determine the radio button that's activated
- action = self.tk_selected_action.get()
- if prev:
- action -= 1
- else:
- action += 1
- action %= len(MenuConfig.ACTIONS)
- self.tk_selected_action.set(action)
- def _collect_list_entries(self, start_node, indent=0):
- """
- Given first MenuNode of nodes list at some level in menu hierarchy,
- collects nodes that may be displayed when viewing and editing that
- hierarchy level. Includes implicit menu nodes, i.e. the ones dependent
- on 'config' entry via 'if' statement which are internally represented
- as children of their dependency
- """
- entries = []
- n = start_node
- while n is not None:
- entries.append(ListEntry(self, n, indent))
- # If node refers to a symbol (X) and has children, it is either
- # 'config' or 'menuconfig'. The children are items inside 'if X'
- # block that immediately follows 'config' or 'menuconfig' entry.
- # If it's a 'menuconfig' then corresponding MenuNode is shown as a
- # regular menu entry. But if it's a 'config', then its children need
- # to be shown in the same list with their texts indented
- if (n.list is not None
- and isinstance(n.item, kconfiglib.Symbol)
- and n.is_menuconfig == False):
- entries.extend(
- self._collect_list_entries(n.list, indent=indent + 1)
- )
- n = n.next
- return entries
- def refresh_display(self, reset_selection=False):
- # Refresh list entries' attributes
- for e in self.all_entries:
- e.refresh()
- # Try to preserve selection upon refresh
- selected_entry = self._selected_entry
- # Also try to preserve listbox scroll offset
- # If not preserved, the see() method will make wanted item to appear
- # at the bottom of the list, even if previously it was in center
- scroll_offset = self.list.yview()[0]
- # Show only visible entries
- self.shown_entries = [e for e in self.all_entries if e.visible]
- # Refresh listbox contents
- self.list.delete(0, tk.END)
- self.list.insert(0, *self.shown_entries)
- if selected_entry and not reset_selection:
- # Restore scroll position
- self.list.yview_moveto(scroll_offset)
- # Activate previously selected node
- self._select_node(selected_entry.node)
- else:
- # Select the topmost entry
- self.list.activate(0)
- self._invert_colors(0)
- # Select ACTION_SELECT on each refresh (mimic C menuconfig)
- self.tk_selected_action.set(self.ACTION_SELECT)
- # Display current location in configuration tree
- pos = []
- for n in self.node_stack + [self.node]:
- pos.append(n.prompt[0] if n.prompt else '[none]')
- self.label_position['text'] = u'# ' + u' -> '.join(pos)
- def show_node(self, node):
- self.node = node
- if node.list is not None:
- self.all_entries = self._collect_list_entries(node.list)
- else:
- self.all_entries = []
- self.refresh_display(reset_selection=True)
- def show_submenu(self, node):
- self.node_stack.append(self.node)
- self.show_node(node)
- def show_parent(self):
- if self.node_stack:
- select_node = self.node
- parent_node = self.node_stack.pop()
- self.show_node(parent_node)
- # Restore previous selection
- self._select_node(select_node)
- self.refresh_display(reset_selection=False)
- def ask_for_string(self, ident=None, title='Enter string', value=None):
- """
- Raises dialog with text entry widget and asks user to enter string
- Return:
- - str - user entered string
- - None - entry was cancelled
- """
- text = 'Please enter a string value\n' \
- 'User <Enter> key to accept the value\n' \
- 'Use <Esc> key to cancel entry\n'
- d = EntryDialog(self.root, text, title, ident=ident, value=value)
- self.root.wait_window(d.dlg)
- self.list.focus_set()
- return d.value
- def ask_for_int(self, ident=None, title='Enter integer value', value=None, ranges=()):
- """
- Raises dialog with text entry widget and asks user to enter decimal number
- Ranges should be iterable of tuples (start, end),
- where 'start' and 'end' specify allowed value range (inclusively)
- Return:
- - int - when valid number that falls within any one of specified ranges is entered
- - None - invalid number or entry was cancelled
- """
- text = 'Please enter a decimal value. Fractions will not be accepted\n' \
- 'User <Enter> key to accept the value\n' \
- 'Use <Esc> key to cancel entry\n'
- d = EntryDialog(self.root, text, title, ident=ident, value=value)
- self.root.wait_window(d.dlg)
- self.list.focus_set()
- ivalue = None
- if d.value:
- try:
- ivalue = int(d.value)
- except ValueError:
- messagebox.showerror('Bad value', 'Entered value \'{}\' is not an integer'.format(d.value))
- if ivalue is not None and ranges:
- allowed = False
- for start, end in ranges:
- allowed = allowed or start <= ivalue and ivalue <= end
- if not allowed:
- messagebox.showerror(
- 'Bad value',
- 'Entered value \'{:d}\' is out of range\n'
- 'Allowed:\n{}'.format(
- ivalue,
- '\n'.join([' {:d} - {:d}'.format(s,e) for s,e in ranges])
- )
- )
- ivalue = None
- return ivalue
- def ask_for_hex(self, ident=None, title='Enter hexadecimal value', value=None, ranges=()):
- """
- Raises dialog with text entry widget and asks user to enter decimal number
- Ranges should be iterable of tuples (start, end),
- where 'start' and 'end' specify allowed value range (inclusively)
- Return:
- - int - when valid number that falls within any one of specified ranges is entered
- - None - invalid number or entry was cancelled
- """
- text = 'Please enter a hexadecimal value\n' \
- 'User <Enter> key to accept the value\n' \
- 'Use <Esc> key to cancel entry\n'
- d = EntryDialog(self.root, text, title, ident=ident, value=value)
- self.root.wait_window(d.dlg)
- self.list.focus_set()
- hvalue = None
- if d.value:
- try:
- hvalue = int(d.value, base=16)
- except ValueError:
- messagebox.showerror('Bad value', 'Entered value \'{}\' is not a hexadecimal value'.format(d.value))
- if hvalue is not None and ranges:
- allowed = False
- for start, end in ranges:
- allowed = allowed or start <= hvalue and hvalue <= end
- if not allowed:
- messagebox.showerror(
- 'Bad value',
- 'Entered value \'0x{:x}\' is out of range\n'
- 'Allowed:\n{}'.format(
- hvalue,
- '\n'.join([' 0x{:x} - 0x{:x}'.format(s,e) for s,e in ranges])
- )
- )
- hvalue = None
- return hvalue
- def show_text(self, text, title='Info'):
- """
- Raises dialog with read-only text view that contains supplied text
- """
- d = TextDialog(self.root, text, title)
- self.root.wait_window(d.dlg)
- self.list.focus_set()
- def mark_as_changed(self):
- """
- Marks current config as having unsaved changes
- Should be called whenever config value is changed
- """
- self.unsaved_changes = True
- self.update_status()
- def set_status_string(self, status):
- """
- Sets status string displayed at the bottom of the window
- """
- self.status_string = status
- self.update_status()
- def update_status(self):
- """
- Updates status bar display
- Status bar displays:
- - unsaved status
- - current config path
- - status string (see set_status_string())
- """
- if self.__silent is True:
- return
- self.tk_status.set('{} [{}] {}'.format(
- '<UNSAVED>' if self.unsaved_changes else '',
- self.config_path if self.config_path else '',
- self.status_string
- ))
- def _check_is_visible(self, node):
- v = True
- v = v and node.prompt is not None
- # It should be enough to check if prompt expression is not false and
- # for menu nodes whether 'visible if' is not false
- v = v and kconfiglib.expr_value(node.prompt[1]) > 0
- if node.item == kconfiglib.MENU:
- v = v and kconfiglib.expr_value(node.visibility) > 0
- # If node references Symbol, then we also account for symbol visibility
- # TODO: need to re-think whether this is needed
- if isinstance(node.item, kconfiglib.Symbol):
- if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
- v = v and len(node.item.assignable) > 0
- else:
- v = v and node.item.visibility > 0
- return v
- def config_is_changed(self):
- is_changed = False
- node = self.kconfig.top_node.list
- if not node:
- # Empty configuration
- return is_changed
- while 1:
- item = node.item
- if isinstance(item, kconfiglib.Symbol) and item.user_value is None and self._check_is_visible(node):
- is_changed = True
- print("Config \"# {}\" has changed, need save config file\n".format(node.prompt[0]))
- break;
- # Iterative tree walk using parent pointers
- if node.list:
- node = node.list
- elif node.next:
- node = node.next
- else:
- while node.parent:
- node = node.parent
- if node.next:
- node = node.next
- break
- else:
- break
- return is_changed
- def prevent_losing_changes(self):
- """
- Checks if there are unsaved changes and asks user to save or discard them
- This routine should be called whenever current config is going to be discarded
- Raises the usual 'Yes', 'No', 'Cancel' prompt.
- Return:
- - True: caller may safely drop current config state
- - False: user needs to continue work on current config ('Cancel' pressed or saving failed)
- """
- if self.config_is_changed() == True:
- self.mark_as_changed()
- if not self.unsaved_changes:
- return True
-
- if self.__silent:
- saved = self.save_config()
- return saved
- res = messagebox.askyesnocancel(
- parent=self.root,
- title='Unsaved changes',
- message='Config has unsaved changes. Do you want to save them?'
- )
- if res is None:
- return False
- elif res is False:
- return True
- # Otherwise attempt to save config and succeed only if config has been saved successfully
- saved = self.save_config()
- return saved
- def open_config(self, path=None):
- if path is None:
- # Create open dialog. Either existing file is selected or no file is selected as a result
- path = filedialog.askopenfilename(
- parent=self.root,
- title='Open config..',
- initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
- filetypes=(('.config files', '*.config'), ('All files', '*.*'))
- )
- if not path or not os.path.isfile(path):
- return False
- path = os.path.abspath(path)
- print('Loading config: \'{}\''.format(path))
- # Try to open given path
- # If path does not exist, we still set current config path to it but don't load anything
- self.unsaved_changes = False
- self.config_path = path
- if not os.path.exists(path):
- self.set_status_string('New config')
- self.mark_as_changed()
- return True
- # Load config and set status accordingly
- try:
- self.kconfig.load_config(path)
- except IOError as e:
- self.set_status_string('Failed to load: \'{}\''.format(path))
- if not self.__silent:
- self.refresh_display()
- print('Failed to load config \'{}\': {}'.format(path, e))
- return False
- self.set_status_string('Opened config')
- if not self.__silent:
- self.refresh_display()
- return True
- def save_config(self, force_file_dialog=False):
- path = self.config_path
- if path is None or force_file_dialog:
- path = filedialog.asksaveasfilename(
- parent=self.root,
- title='Save config as..',
- initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
- initialfile=os.path.basename(self.config_path) if self.config_path else None,
- defaultextension='.config',
- filetypes=(('.config files', '*.config'), ('All files', '*.*'))
- )
- if not path:
- return False
- path = os.path.abspath(path)
- print('Saving config: \'{}\''.format(path))
- # Try to save config to selected path
- try:
- self.kconfig.write_config(path, header="#\n# Automatically generated file; DO NOT EDIT.\n")
- self.unsaved_changes = False
- self.config_path = path
- self.set_status_string('Saved config')
- except IOError as e:
- self.set_status_string('Failed to save: \'{}\''.format(path))
- print('Save failed: {}'.format(e), file=sys.stderr)
- return False
- return True
- def _center_window(root, window):
- # type: (tk.Tk, tk.Toplevel) -> None
- """
- Attempts to center window on screen
- """
- root.update_idletasks()
- # root.eval('tk::PlaceWindow {!s} center'.format(
- # window.winfo_pathname(window.winfo_id())
- # ))
- w = window.winfo_width()
- h = window.winfo_height()
- ws = window.winfo_screenwidth()
- hs = window.winfo_screenheight()
- x = (ws / 2) - (w / 2)
- y = (hs / 2) - (h / 2)
- window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
- window.lift()
- window.focus_force()
- def _center_window_above_parent(root, window):
- # type: (tk.Tk, tk.Toplevel) -> None
- """
- Attempts to center window above its parent window
- """
- # root.eval('tk::PlaceWindow {!s} center'.format(
- # window.winfo_pathname(window.winfo_id())
- # ))
- root.update_idletasks()
- parent = window.master
- w = window.winfo_width()
- h = window.winfo_height()
- px = parent.winfo_rootx()
- py = parent.winfo_rooty()
- pw = parent.winfo_width()
- ph = parent.winfo_height()
- x = px + (pw / 2) - (w / 2)
- y = py + (ph / 2) - (h / 2)
- window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
- window.lift()
- window.focus_force()
- def main(argv=None):
- if argv is None:
- argv = sys.argv[1:]
- # Instantiate cmd options parser
- parser = argparse.ArgumentParser(
- description='Interactive Kconfig configuration editor'
- )
- parser.add_argument(
- '--kconfig',
- metavar='FILE',
- type=str,
- default='Kconfig',
- help='path to root Kconfig file'
- )
- parser.add_argument(
- '--config',
- metavar='FILE',
- type=str,
- help='path to .config file to load'
- )
- if "--silent" in argv:
- parser.add_argument(
- '--silent',
- dest = '_silent_',
- type=str,
- help='silent mode, not show window'
- )
- args = parser.parse_args(argv)
- kconfig_path = args.kconfig
- config_path = args.config
- # Verify that Kconfig file exists
- if not os.path.isfile(kconfig_path):
- raise RuntimeError('\'{}\': no such file'.format(kconfig_path))
- # Parse Kconfig files
- kconf = kconfiglib.Kconfig(filename=kconfig_path)
- if "--silent" not in argv:
- print("In normal mode. Will show menuconfig window.")
- mc = MenuConfig(kconf)
- # If config file was specified, load it
- if config_path:
- mc.open_config(config_path)
- print("Enter mainloop. Waiting...")
- tk.mainloop()
- else:
- print("In silent mode. Don`t show menuconfig window.")
- mc = MenuConfig(kconf, True)
- # If config file was specified, load it
- if config_path:
- mc.open_config(config_path)
- mc._close_window()
- if __name__ == '__main__':
- main()
|