diff --git a/tools/building.py b/tools/building.py index f49a49e268..f19b76d0e3 100644 --- a/tools/building.py +++ b/tools/building.py @@ -377,29 +377,19 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [ dest = 'pyconfig', action = 'store_true', default = False, - help = 'Python ASCII menuconfig for RT-Thread BSP') + help = 'Python GUI menuconfig for RT-Thread BSP') AddOption('--pyconfig-silent', dest = 'pyconfig_silent', action = 'store_true', default = False, help = 'Don`t show pyconfig window') - AddOption('--guiconfig', - dest = 'guiconfig', - action = 'store_true', - default = False, - help = 'Python GUI menuconfig for RT-Thread BSP') if GetOption('pyconfig_silent'): - from menuconfig import pyconfig_silent + from menuconfig import guiconfig_silent - pyconfig_silent(Rtt_Root) + guiconfig_silent(Rtt_Root) exit(0) elif GetOption('pyconfig'): - from menuconfig import pyconfig - - pyconfig(Rtt_Root) - exit(0) - elif GetOption('guiconfig'): from menuconfig import guiconfig guiconfig(Rtt_Root) diff --git a/tools/menuconfig.py b/tools/menuconfig.py index f533360d7b..d904e8a783 100644 --- a/tools/menuconfig.py +++ b/tools/menuconfig.py @@ -226,51 +226,6 @@ def menuconfig(RTT_ROOT): if mtime != mtime2: mk_rtconfig(fn) -# pyconfig for windows and linux -def pyconfig(RTT_ROOT): - import pymenuconfig - - touch_env() - env_dir = get_env_dir() - - os.environ['PKGS_ROOT'] = os.path.join(env_dir, 'packages') - - fn = '.config' - - if os.path.isfile(fn): - mtime = os.path.getmtime(fn) - else: - mtime = -1 - - pymenuconfig.main(['--kconfig', 'Kconfig', '--config', '.config']) - - if os.path.isfile(fn): - mtime2 = os.path.getmtime(fn) - else: - mtime2 = -1 - - # make rtconfig.h - if mtime != mtime2: - mk_rtconfig(fn) - - -# pyconfig_silent for windows and linux -def pyconfig_silent(RTT_ROOT): - import pymenuconfig - print("In pyconfig silent mode. Don`t display menuconfig window.") - - touch_env() - env_dir = get_env_dir() - - os.environ['PKGS_ROOT'] = os.path.join(env_dir, 'packages') - - fn = '.config' - - pymenuconfig.main(['--kconfig', 'Kconfig', '--config', '.config', '--silent', 'True']) - - # silent mode, force to make rtconfig.h - mk_rtconfig(fn) - # guiconfig for windows and linux def guiconfig(RTT_ROOT): import pyguiconfig diff --git a/tools/pymenuconfig.py b/tools/pymenuconfig.py deleted file mode 100644 index 67893d7281..0000000000 --- a/tools/pymenuconfig.py +++ /dev/null @@ -1,1206 +0,0 @@ -# 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 '' key is pressed in - a main window. Also selects choice value. - - select(): - Called when '' 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 '' 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 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 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 '', 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 '')) - 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('', self.accept) - self.dlg.bind('', 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('', self.accept) - self.dlg.bind('', 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. performs selected operation (set of buttons at the bottom)', - 'Pressing includes, excludes, modularizes features', - 'Press to go one level up. Press at top level to exit', - 'Legend: [*] built-in [ ] excluded 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('', self.handle_keypress) - dlg.bind('', self.handle_keypress) - dlg.bind('', self.handle_keypress) - dlg.bind('', self.handle_keypress) - dlg.bind('', self.handle_keypress) - dlg.bind('', self.handle_keypress) - dlg.bind('', 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 key to accept the value\n' \ - 'Use 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 key to accept the value\n' \ - 'Use 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 key to accept the value\n' \ - 'Use 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( - '' 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()