xref: /nrf52832-nimble/rt-thread/tools/pymenuconfig.py (revision 104654410c56c573564690304ae786df310c91fc)
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