diff --git a/TermTk/TTkCore/__init__.py b/TermTk/TTkCore/__init__.py index 03f2bf99..45204823 100644 --- a/TermTk/TTkCore/__init__.py +++ b/TermTk/TTkCore/__init__.py @@ -8,6 +8,7 @@ from .propertyanimation import * from .ttk import * from .canvas import * from .color import * +from .shortcut import * from .string import * from .timer import * from .filebuffer import * diff --git a/TermTk/TTkCore/helper.py b/TermTk/TTkCore/helper.py index 236076e5..ed15947a 100644 --- a/TermTk/TTkCore/helper.py +++ b/TermTk/TTkCore/helper.py @@ -53,26 +53,6 @@ class TTkHelper: widget.move(x,y) _overlay = [] - class _Shortcut(): - __slots__ = ('_letter','_widget') - def __init__(self, letter, widget): - self._letter = letter.lower() - self._widget = widget - _shortcut = [] - - @staticmethod - def addShortcut(widget, letter): - TTkHelper._shortcut.append(TTkHelper._Shortcut(letter, widget)) - - @staticmethod - def execShortcut(letter, widget=None): - if not isinstance(letter, str): return - for sc in TTkHelper._shortcut: - if sc._letter == letter.lower() and sc._widget.isVisibleAndParent(): - if not widget or TTkHelper.isParent(widget, sc._widget): - sc._widget.shortcutEvent() - return - @staticmethod def updateAll(): if TTkHelper._rootWidget: @@ -292,8 +272,8 @@ class TTkHelper: # Build a list of buffers to be repainted updateWidgetsBk = TTkHelper._updateWidget.copy() updateBuffers = TTkHelper._updateBuffer.copy() - TTkHelper._updateWidget = set() - TTkHelper._updateBuffer = set() + TTkHelper._updateWidget.clear() + TTkHelper._updateBuffer.clear() updateWidgets = set() # TTkLog.debug(f"{len(TTkHelper._updateBuffer)} {len(TTkHelper._updateWidget)}") diff --git a/TermTk/TTkCore/shortcut.py b/TermTk/TTkCore/shortcut.py index ec461e79..457a02de 100644 --- a/TermTk/TTkCore/shortcut.py +++ b/TermTk/TTkCore/shortcut.py @@ -22,6 +22,7 @@ __all__ = ['TTkShortcut'] +from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.helper import TTkHelper from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal @@ -41,6 +42,12 @@ class TTkKeySequence(): key &= ~(TTkK.CTRL|TTkK.ALT|TTkK.SHIFT|TTkK.META) t = TTkK.SpecialKey if mod else TTkK.Character self._key = TTkKeyEvent(type=t, key=key, mod=mod, code="") + if mod: + self._key = TTkKeyEvent(mod=mod, code="", type=TTkK.SpecialKey, key=key ) + else: + self._key = TTkKeyEvent(mod=mod, code="", type=TTkK.Character, key=chr(key) ) + + def __hash__(self) -> int: return self._key.__hash__() @@ -70,8 +77,15 @@ class TTkShortcut(): @staticmethod def processKey(key, focusWidget): + # TTkLog.debug(f"{str(key)=}") + # for k in TTkShortcut._shortcuts: + # TTkLog.debug(f"{str(k)=} - {key==k=}") if key in TTkShortcut._shortcuts: for sc in TTkShortcut._shortcuts[key]: + # if sc._parent: + # TTkLog.debug(f"{focusWidget=} {sc._parent=} {sc._parent._parent=}") + # else: + # TTkLog.debug(f"{focusWidget=} {sc._parent=}") if ( ( sc._shortcutContext == TTkK.WidgetShortcut and focusWidget == sc._parent ) or ( sc._shortcutContext == TTkK.WidgetWithChildrenShortcut diff --git a/TermTk/TTkCore/string.py b/TermTk/TTkCore/string.py index b8f1f180..98b41b8b 100644 --- a/TermTk/TTkCore/string.py +++ b/TermTk/TTkCore/string.py @@ -373,6 +373,23 @@ class TTkString(): return ret + def extractShortcuts(self): + def _chGenerator(): + for ch,color in zip(self._text,self._colors): + yield ch,color + _newText = "" + _newColors = [] + _ret = [] + _gen = _chGenerator() + for ch,color in _gen: + if ch == '&': + ch,color = next(_gen) + _ret.append(ch) + color += TTkColor.UNDERLINE + _newText += ch + _newColors.append(color) + return TTkString._importString1(_newText,_newColors), _ret + def replace(self, *args, **kwargs): ''' **replace** (*old*, *new*, *count*) diff --git a/TermTk/TTkWidgets/menu.py b/TermTk/TTkWidgets/menu.py index aa290a33..ac07fc9c 100644 --- a/TermTk/TTkWidgets/menu.py +++ b/TermTk/TTkWidgets/menu.py @@ -74,12 +74,6 @@ class TTkMenuButton(TTkWidget): self._checkable = checkable self._shortcuts = [] self._highlighted = False - while self._text.find('&') != -1: - index = self.text().find('&') - shortcut = self.text().charAt(index+1) - TTkHelper.addShortcut(self, shortcut) - self._shortcuts.append(index) - self.setText(self.text().substring(to=index)+self.text().substring(fr=index+1)) super().__init__(**kwargs) width = self._text.termWidth() + (3 if self._checkable else 1) self.setMinimumWidth(width) @@ -161,6 +155,7 @@ class TTkMenuButton(TTkWidget): self.textChanged.emit(self._text) self.update() + @pyTTkSlot() def shortcutEvent(self): self._triggerButton() @@ -213,7 +208,10 @@ class TTkMenuButton(TTkWidget): def addMenu(self, text:TTkString, data:object=None, checkable:bool=False, checked:bool=False): '''addMenu''' + text = text if issubclass(type(text),TTkString) else TTkString(text) + text, shortcuts = text.extractShortcuts() button = TTkMenuButton(text=text, data=data, checkable=checkable, checked=checked) + button._shortcuts = shortcuts self._submenu.append(button) return button @@ -235,8 +233,6 @@ class TTkMenuButton(TTkWidget): if self._submenu: canvas._set(0, w-1, '▶', style['color']) off = 0 - for i in self._shortcuts: - canvas._set(0,i+off, self._text.charAt(i), TTkColor.UNDERLINE) class _TTkMenuAreaWidget(TTkAbstractScrollView): __slots__ = ('_submenu','_minWith','_caller') @@ -269,9 +265,9 @@ class _TTkMenuAreaWidget(TTkAbstractScrollView): def keyEvent(self, evt) -> bool: if not self._submenu: return False + btns = [b for b in self._submenu if type(b)==TTkMenuButton] if evt.type == TTkK.SpecialKey: # Retrieve the current highlighted button - btns = [b for b in self._submenu if type(b)==TTkMenuButton] curBtn = _b[0] if (_b := [b for b in btns if b._highlighted]) else None if evt.key == TTkK.Key_Up: self._cleanHighlight() @@ -300,6 +296,13 @@ class _TTkMenuAreaWidget(TTkAbstractScrollView): if curBtn: curBtn._triggerSubmenu() return True + else: + # Handle shortcuts + ch = evt.key + for btn in btns: + if ch in btn._shortcuts: + btn.shortcutEvent() + return True return super().keyEvent(evt) def resizeEvent(self, w, h): diff --git a/TermTk/TTkWidgets/menubar.py b/TermTk/TTkWidgets/menubar.py index 879ddc80..6e52920a 100644 --- a/TermTk/TTkWidgets/menubar.py +++ b/TermTk/TTkWidgets/menubar.py @@ -28,6 +28,7 @@ from TermTk.TTkCore.color import TTkColor # from TermTk.TTkCore.log import TTkLog from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot from TermTk.TTkCore.string import TTkString +from TermTk.TTkCore.shortcut import TTkShortcut from TermTk.TTkLayouts.layout import TTkLayout from TermTk.TTkLayouts.boxlayout import TTkHBoxLayout from TermTk.TTkWidgets.menu import TTkMenuButton @@ -41,14 +42,7 @@ class TTkMenuBarButton(TTkMenuButton): __slots__=('_shortcut') def __init__(self, *, text=..., data=None, checkable=False, checked=False, **kwargs): self._shortcut = [] - super().__init__(text=text, data=data, checkable=checkable, checked=checked, **kwargs) - while self.text().find('&') != -1: - index = self.text().find('&') - shortcut = self.text().charAt(index+1) - TTkHelper.addShortcut(self, shortcut) - self._shortcut.append(index) - self.setText(self.text().substring(to=index)+self.text().substring(fr=index+1)) - txtlen = self.text().termWidth() + super().__init__(text=text, data=data, checkable=checkable, checked=checked, shortcutPrefix=TTkK.ALT, **kwargs) self.setCheckable(self.isCheckable()) def setCheckable(self, ch): @@ -101,7 +95,12 @@ class TTkMenuBarLayout(TTkHBoxLayout): def addMenu(self,text:TTkString, data:object=None, checkable:bool=False, checked:bool=False, alignment=TTkK.LEFT_ALIGN): '''addMenu''' + text = text if issubclass(type(text),TTkString) else TTkString(text) + text, shortcuts = text.extractShortcuts() button = TTkMenuBarButton(text=text, data=data, checkable=checkable, checked=checked) + for ch in shortcuts: + shortcut = TTkShortcut(key=TTkK.ALT | ord(ch.upper())) + shortcut.activated.connect(button.shortcutEvent) self._mbItems(alignment).addWidget(button) self._buttons.append(button) self.update() diff --git a/tests/t.ui/test.ui.031.shortcut.02.menu.py b/tests/t.ui/test.ui.031.shortcut.02.menu.py new file mode 100755 index 00000000..a3a7484a --- /dev/null +++ b/tests/t.ui/test.ui.031.shortcut.02.menu.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2023 Eugenio Parodi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys, os +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +class WindowThatHandleKeypress(ttk.TTkWindow): + def keyEvent(self, evt) -> bool: + if evt.mod==ttk.TTkK.ControlModifier: + if evt.key == ttk.TTkK.Key_F: + ttk.TTkLog.debug("Pressed Key CTRL F inside the Window") + return True + return super().keyEvent(evt) + +def setMenu(curMenu:ttk.TTkMenuButton): + curMenu.addMenu("New File") + curMenu.addMenu("Old File") + curMenu.addSpacer() + curMenu.addMenu("Open",checkable=True) + curMenu.addMenu("Save",checkable=True,checked=True) + curMenu.addMenu("Save as").setDisabled() + curMenu.addSpacer() + exportFileMenu = curMenu.addMenu("E&xport") + txtExportFileMenu = exportFileMenu.addMenu("t&xt") + txtExportFileMenu.addMenu("ASCII") + txtExportFileMenu.addMenu("URF-8") + txtExportFileMenu.addMenu("PETSCII") + exportFileMenu = curMenu.addMenu("E&xport 2") + txtExportFileMenu = exportFileMenu.addMenu("t&xt 2") + txtExportFileMenu.addMenu("ASCII 2") + txtExportFileMenu.addMenu("URF-8 2") + txtExportFileMenu.addMenu("PETSCII 2") + exportFileMenu.addMenu("&json") + exportFileMenu.addMenu("&yaml") + curMenu.addSpacer() + curMenu.addMenu("Closeeeeeeeee1234567890") + curMenu.addMenu("Close") + curMenu.addSpacer() + curMenu.addMenu("Exit") + +root = ttk.TTk(mouseTrack=True) + +window = ttk.TTkWindow(title="Test MenuBar", parent=root,pos=(30,1), size=(60,10), border=True) +menuTop = ttk.TTkMenuBarLayout() +setMenu(menuTop.addMenu("&File")) +window.setMenuBar(menuTop) +#menuBottom = ttk.TTkMenuBarLayout() +#setMenu(menuBottom.addMenu("&Fi&le")) +#window.setMenuBar(menuBottom, ttk.TTkK.BOTTOM) + +window = WindowThatHandleKeypress(title="Handle CTRL F if focused", parent=root, pos=(0,5), size=(40,10)) +#menuTop = ttk.TTkMenuBarLayout() +#setMenu(menuTop.addMenu("&File")) +#window.setMenuBar(menuTop) +#menuBottom = ttk.TTkMenuBarLayout() +#setMenu(menuBottom.addMenu("&Fi&le")) +#window.setMenuBar(menuBottom, ttk.TTkK.BOTTOM) + +logWin = ttk.TTkWindow(title="LOG", parent=root, pos=(20,10), size=(100,20), border=True, layout=ttk.TTkGridLayout()) +ttk.TTkLogViewer(parent=logWin) + +WindowThatHandleKeypress(title="Handle CTRL F if focused", parent=root, pos=(0,15), size=(40,10)) + +sc = ttk.TTkShortcut(ttk.TTkK.ALT | ttk.TTkK.Key_A) +sc.activated.connect(lambda : ttk.TTkLog.debug("Pressed Key Alt A")) + +sc = ttk.TTkShortcut(ttk.TTkK.ALT | ttk.TTkK.Key_B) +sc.activated.connect(lambda : ttk.TTkLog.debug("Pressed Key Alt B")) + +sc = ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.Key_F) +sc.activated.connect(lambda : ttk.TTkLog.debug("Pressed Key CTRL F")) + +sc = ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.ALT | ttk.TTkK.Key_F) +sc.activated.connect(lambda : ttk.TTkLog.debug("Pressed Key CTRL ALT F")) + +sc = ttk.TTkShortcut(ttk.TTkK.CTRL | ttk.TTkK.ALT | ttk.TTkK.SHIFT | ttk.TTkK.Key_D) # it depend on the terminal used +sc.activated.connect(lambda : ttk.TTkLog.debug("Pressed Key CTRL ALT SHIFT D")) # it depend if the terminal allows it + +root.mainloop() \ No newline at end of file