From 1e3ac92670145ca808f3477715c7617688046724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Tue, 7 Jan 2025 12:00:42 +0000 Subject: [PATCH 1/6] Included basic viewFullAreaSize implementation in TTkAbstractScrollView --- TermTk/TTkAbstract/abstractscrollview.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/TermTk/TTkAbstract/abstractscrollview.py b/TermTk/TTkAbstract/abstractscrollview.py index d25fe8fd..080a5796 100644 --- a/TermTk/TTkAbstract/abstractscrollview.py +++ b/TermTk/TTkAbstract/abstractscrollview.py @@ -176,6 +176,11 @@ def __init__(self, **kwargs) -> None: def viewDisplayedSize(self) -> tuple[int,int]: return self.size() + def viewFullAreaSize(self) -> tuple[int,int]: + t,b,l,r = self.getPadding() + _,_,w,h = self.layout().fullWidgetAreaGeometry() + return w+l+r, h+t+b + @pyTTkSlot(int, int) def viewMoveTo(self, x: int, y: int): fw, fh = self.viewFullAreaSize() @@ -216,6 +221,10 @@ def update(self, repaint=True, updateLayout=False, updateParent=False): self.viewChanged.emit() return super().update(repaint, updateLayout, updateParent) + def setPadding(self, top, bottom, left, right) -> None: + super().setPadding(top, bottom, left, right) + self.viewChanged.emit() + class TTkAbstractScrollViewLayout(TTkLayout, TTkAbstractScrollViewInterface): ''' :py:class:`TTkAbstractScrollViewLayout` From 20989a4d04e0f9f2411b1d195539231292889392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Tue, 7 Jan 2025 12:10:51 +0000 Subject: [PATCH 2/6] Removed the viewFullAreaSize implementations that are allready handled in the default implementation --- TermTk/TTkWidgets/TTkPickers/textpicker.py | 4 ---- TermTk/TTkWidgets/menu.py | 4 ---- TermTk/TTkWidgets/scrollarea.py | 4 ---- multiplexers/workbench/wblib/scrollwin.py | 7 ------- tools/dumbPaintTool/app/layersctrl.py | 4 ---- tools/ttkDesigner/app/menuBarEditor.py | 14 -------------- 6 files changed, 37 deletions(-) diff --git a/TermTk/TTkWidgets/TTkPickers/textpicker.py b/TermTk/TTkWidgets/TTkPickers/textpicker.py index fb1bd667..43aa7c5d 100644 --- a/TermTk/TTkWidgets/TTkPickers/textpicker.py +++ b/TermTk/TTkWidgets/TTkPickers/textpicker.py @@ -107,10 +107,6 @@ def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) - def viewFullAreaSize(self) -> tuple[int,int]: - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w , h - def maximumWidth(self): return 0x10000 def maximumHeight(self): return 0x10000 def minimumWidth(self): return 0 diff --git a/TermTk/TTkWidgets/menu.py b/TermTk/TTkWidgets/menu.py index d5641d82..49d59114 100644 --- a/TermTk/TTkWidgets/menu.py +++ b/TermTk/TTkWidgets/menu.py @@ -344,10 +344,6 @@ def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) - def viewFullAreaSize(self) -> tuple: - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w , h - def maximumWidth(self): return 0x10000 def maximumHeight(self): return 0x10000 def minimumWidth(self): return 0 diff --git a/TermTk/TTkWidgets/scrollarea.py b/TermTk/TTkWidgets/scrollarea.py index da2c1a2c..2c8259ab 100644 --- a/TermTk/TTkWidgets/scrollarea.py +++ b/TermTk/TTkWidgets/scrollarea.py @@ -39,10 +39,6 @@ def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) - def viewFullAreaSize(self) -> tuple[int,int]: - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w , h - def maximumWidth(self): return 0x10000 def maximumHeight(self): return 0x10000 def minimumWidth(self): return 0 diff --git a/multiplexers/workbench/wblib/scrollwin.py b/multiplexers/workbench/wblib/scrollwin.py index 48ae9da6..479ba040 100644 --- a/multiplexers/workbench/wblib/scrollwin.py +++ b/multiplexers/workbench/wblib/scrollwin.py @@ -66,13 +66,6 @@ def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) - def viewFullAreaSize(self) -> (int, int): - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w , h - - def viewDisplayedSize(self) -> (int, int): - return self.size() - def maximumWidth(self): return 0x10000 def maximumHeight(self): return 0x10000 def minimumWidth(self): return 0 diff --git a/tools/dumbPaintTool/app/layersctrl.py b/tools/dumbPaintTool/app/layersctrl.py index bc505503..3ef92934 100644 --- a/tools/dumbPaintTool/app/layersctrl.py +++ b/tools/dumbPaintTool/app/layersctrl.py @@ -187,10 +187,6 @@ def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) - def viewFullAreaSize(self) -> tuple: - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w,h - def viewDisplayedSize(self) -> tuple: return self.size() diff --git a/tools/ttkDesigner/app/menuBarEditor.py b/tools/ttkDesigner/app/menuBarEditor.py index e3d31376..b83af597 100644 --- a/tools/ttkDesigner/app/menuBarEditor.py +++ b/tools/ttkDesigner/app/menuBarEditor.py @@ -243,13 +243,6 @@ def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) - def viewFullAreaSize(self) -> tuple: - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w , h - - def viewDisplayedSize(self) -> tuple: - return self.size() - def maximumWidth(self): return 0x10000 def maximumHeight(self): return 0x10000 def minimumWidth(self): return 0 @@ -291,13 +284,6 @@ def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) - def viewFullAreaSize(self) -> (int, int): - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w, h - - def viewDisplayedSize(self) -> (int, int): - return self.size() - def _importMenuItem(self,menuButton): item = _MenuItem(menuButton=menuButton, autoResize=True, designer=self._designer) self._items.append(item) From cef3b48082556e43e0cb32dad678bcb594ae80b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Tue, 7 Jan 2025 13:19:29 +0000 Subject: [PATCH 3/6] Added searchText in the list widget --- TermTk/TTkWidgets/listwidget.py | 120 +++++++++++++++--- .../02.array.09.List.conditional.splitting.py | 92 ++++++++++++++ 2 files changed, 193 insertions(+), 19 deletions(-) create mode 100755 tests/timeit/02.array.09.List.conditional.splitting.py diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 4bac2315..393f297c 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -160,19 +160,26 @@ class TTkListWidget(TTkAbstractScrollView): :type text: str ''' + classStyle = { + 'default': {'searchColor': TTkColor.fg("#FFFF00") + TTkColor.UNDERLINE}, + } + @dataclass(frozen=True) class _DropListData: widget: TTkAbstractScrollView items: list - __slots__ = ('itemClicked', 'textClicked', - '_selectedItems', '_selectionMode', - '_highlighted', '_items', - '_dragPos', '_dndMode') + __slots__ = ('_selectedItems', '_selectionMode', + '_highlighted', '_items', '_filteredItems', + '_dragPos', '_dndMode', + '_searchText', '_showSearch', + # Signals + 'itemClicked', 'textClicked','searchModified') def __init__(self, *, items:list[str]=[], selectionMode:int=TTkK.SelectionMode.SingleSelection, dragDropMode:TTkK.DragDropMode=TTkK.DragDropMode.NoDragDrop, + showSearch:bool=True, **kwargs) -> None: ''' :param items: Use this field to intialize the :py:class:`TTkListWidget` with the entries in the items list, defaults to "[]" @@ -181,23 +188,30 @@ def __init__(self, *, :type selectionMode: :py:class:`TTkK.SelectionMode`, optional :param dragDropMode: This property holds the drag and drop event the view will act upon, defaults to :py:class:`TTkK.DragDropMode.NoDragDrop`. :type dragDropMode: :py:class:`TTkK.DragDropMode`, optional + :param showSearch: Show the search hint at the top of the list, defaults to True. + :type showSearch: bool, optional ''' # Signals self.itemClicked = pyTTkSignal(TTkAbstractListItem) self.textClicked = pyTTkSignal(str) + self.searchModified = pyTTkSignal(str) # Default Class Specific Values self._selectionMode = selectionMode - self._selectedItems = [] - self._items = [] + self._selectedItems:list[TTkAbstractListItem] = [] + self._items:list[TTkAbstractListItem]= [] + self._filteredItems:list[TTkAbstractListItem] = self._items self._highlighted = None self._dragPos = None self._dndMode = dragDropMode + self._searchText:str = '' + self._showSearch:bool = showSearch # Init Super super().__init__(**kwargs) self.addItems(items) self.viewChanged.connect(self._viewChangedHandler) self.setFocusPolicy(TTkK.ClickFocus + TTkK.TabFocus) + self.searchModified.connect(self._searchModifiedHandler) @pyTTkSlot() def _viewChangedHandler(self): @@ -226,6 +240,33 @@ def _labelSelectedHandler(self, label:TTkAbstractListItem): self._highlighted = label self.itemClicked.emit(label) self.textClicked.emit(label.text()) + + @pyTTkSlot(str) + def _searchModifiedHandler(self, text:str='s') -> None: + if self._showSearch and self._searchText: + self.setPadding(1,0,0,0) + else: + self.setPadding(0,0,0,0) + + if self._searchText: + text = self._searchText.lower() + self._filteredItems = [i for i in self._items if text in str(i.text()).lower()] + for item in self._items: + item.setVisible(text in str(item.text()).lower()) + else: + self._filteredItems = self._items + for item in self._items: + item.setVisible(True) + + self._placeItems() + + def showSearch(self) -> bool: + '''showSearch''' + return self._showSearch + + def setShowSearch(self, showSearch:bool) -> None: + '''setShowSearch''' + self._showSearch = showSearch def dragDropMode(self): '''dragDropMode''' @@ -251,10 +292,14 @@ def selectedLabels(self): '''selectedLabels''' return [i.text() for i in self._selectedItems] - def items(self): + def items(self) -> list[TTkAbstractListItem]: '''items''' return self._items + def filteredItems(self) -> list[TTkAbstractListItem]: + '''filteredItems''' + return self._filteredItems + def resizeEvent(self, w, h): maxw = 0 for item in self.layout().children(): @@ -265,10 +310,6 @@ def resizeEvent(self, w, h): item.setGeometry(x,y,maxw,h) TTkAbstractScrollView.resizeEvent(self, w, h) - def viewFullAreaSize(self) -> tuple[int,int]: - _,_,w,h = self.layout().fullWidgetAreaGeometry() - return w, h - def addItem(self, item, data=None): '''addItem''' self.addItemAt(item, len(self._items), data) @@ -280,8 +321,11 @@ def addItems(self, items): def _placeItems(self): minw = self.width() for item in self._items: - minw = max(minw,item.minimumWidth()) - for y,item in enumerate(self._items): + if item in self._filteredItems: + minw = max(minw,item.minimumWidth()) + else: + item.setGeometry(0,0,0,0) + for y,item in enumerate(self._filteredItems): item.setGeometry(0,y,minw,1) self.viewChanged.emit() self.update() @@ -366,6 +410,9 @@ def _moveToHighlighted(self): elif index <= offy: self.viewMoveTo(offx, index) + def mousePressEvent(self, evt:TTkMouseEvent) -> bool: + return True + def mouseDragEvent(self, evt:TTkMouseEvent) -> bool: if not(self._dndMode & TTkK.DragDropMode.AllowDrag): return False @@ -416,25 +463,45 @@ def dropEvent(self, evt:TTkDnDEvent) -> bool: self._dragPos = None if not issubclass(type(evt.data()) ,TTkListWidget._DropListData): return False + t,b,l,r = self.getPadding() offx,offy = self.getViewOffsets() wid = evt.data().widget items = evt.data().items if wid and items: wid.removeItems(items) + wid._searchModifiedHandler() for it in items: it.setCurrentStyle(it.style()['default']) - self.addItemsAt(items,offy+evt.y) + yPos = offy+evt.y-t + if self._filteredItems: + if yPos < 0: + yPos = 0 + elif yPos > len(self._filteredItems): + yPos = len(self._items) + elif yPos == len(self._filteredItems): + filteredItemAt = self._filteredItems[-1] + yPos = self._items.index(filteredItemAt)+1 + else: + filteredItemAt = self._filteredItems[yPos] + yPos = self._items.index(filteredItemAt) + else: + yPos = 0 + self.addItemsAt(items,yPos) + self._searchModifiedHandler() return True return False def keyEvent(self, evt:TTkKeyEvent) -> bool: if not self._highlighted: return False - if ( evt.type == TTkK.Character and evt.key==" " ) or \ + if ( not self._searchText and evt.type == TTkK.Character and evt.key==" " ) or \ ( evt.type == TTkK.SpecialKey and evt.key == TTkK.Key_Enter ): if self._highlighted: # TTkLog.debug(self._highlighted) self._highlighted.listItemClicked.emit(self._highlighted) - return True + elif evt.type == TTkK.Character: + self._searchText += evt.key + self.update() + self.searchModified.emit(self._searchText) elif evt.type == TTkK.SpecialKey: if evt.key == TTkK.Key_Tab: return False @@ -457,13 +524,18 @@ def keyEvent(self, evt:TTkKeyEvent) -> bool: self.viewMoveTo(0, offy) elif evt.key == TTkK.Key_End: self.viewMoveTo(0x10000, offy) - + elif evt.key in (TTkK.Key_Delete,TTkK.Key_Backspace): + if self._searchText: + self._searchText = self._searchText[:-1] + self.update() + self.searchModified.emit(self._searchText) self._highlighted._setHighlighted(False) self._highlighted = self._items[index] self._highlighted._setHighlighted(True) self._moveToHighlighted() - return True - return False + else: + return False + return True def focusInEvent(self): if not self._items: return @@ -488,6 +560,16 @@ def paintChildCanvas(self): p2 = (0,y-offy) canvas.drawText(pos=p1,text="╙─╼", color=TTkColor.fg("#FFFF00")+TTkColor.bg("#008855")) canvas.drawText(pos=p2,text="╓─╼", color=TTkColor.fg("#FFFF00")+TTkColor.bg("#008855")) + + def paintEvent(self, canvas): + if self._showSearch and self._searchText: + w,h = self.size() + if len(self._searchText) > w: + text = f"...{self._searchText[-w-3:]}" + else: + text = self._searchText + color = self.currentStyle()['searchColor'] + canvas.drawText(pos=(0,0),text=text, color=color, width=w) diff --git a/tests/timeit/02.array.09.List.conditional.splitting.py b/tests/timeit/02.array.09.List.conditional.splitting.py new file mode 100755 index 00000000..de61bf4d --- /dev/null +++ b/tests/timeit/02.array.09.List.conditional.splitting.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2024 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 random +import timeit + + +a_base = [f"{random.randint(0x10000,0xffffffff):08x}" for i in range(1000) ] +check = 'ab' + +def test_ti_00(): + a1 = [i for i in a_base if check in i] + a2 = [] # [i for i in a_base if check not in i] + return len(a1),len(a2) + +def test_ti_01(): + a1 = [i for i in a_base if check in i] + a2 = [i for i in a_base if check not in i] + return len(a1),len(a2) + +# def test_ti_02(): +# a1 = [i for i in a_base if check in i] +# a2 = [i for i in a_base if i not in a1] +# return len(a1),len(a2) + +def test_ti_03(): + a1 = [i for i in a_base if check in i] + a2 = 0 + for x in [i for i in a_base if check not in i]: + a2+=1 + return len(a1),a2 + +def test_ti_04(): + a1 = [i for i in a_base if check in i] + a2 = [i for i in a_base if check not in i] + ca1,ca2 = 0,0 + for i in a1: ca1+=1 + for i in a2: ca2+=1 + return ca1,ca2 + +def test_ti_05(): + ca1,ca2 = 0,0 + for i in a_base: + if check in i: + ca1+=1 + else: + ca2+=1 + return ca1,ca2 + +def test_ti_06(): + a1,a2=[],[] + ca1,ca2 = 0,0 + for i in a_base: + if check in i: + ca1+=1 + a1.append(i) + else: + ca2+=1 + a2.append(i) + return ca1,ca2,len(a1),len(a2) + + +loop = 10000 + +a = {} + +for testName in sorted([tn for tn in globals() if tn.startswith('test_ti_')]): + result = timeit.timeit(f'{testName}(*a)', globals=globals(), number=loop) + # print(f"test{iii}) fps {loop / result :.3f} - s {result / loop:.10f} - {result / loop} {globals()[testName](*a)}") + print(f"{testName} | {result / loop:.10f} sec. | {loop / result : 15.3f} Fps ╞╡-> {globals()[testName](*a)}") From efb8cda46a36d53a9da67ad70e8c47d7faed5d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Tue, 7 Jan 2025 16:57:50 +0000 Subject: [PATCH 4/6] Removed bottleneck when list items are filtered --- TermTk/TTkWidgets/list_.py | 3 +- TermTk/TTkWidgets/listwidget.py | 77 ++++++++++++++++-------- tests/t.ui/test.ui.014.list.05.stress.py | 32 ++++++++++ 3 files changed, 86 insertions(+), 26 deletions(-) create mode 100755 tests/t.ui/test.ui.014.list.05.stress.py diff --git a/TermTk/TTkWidgets/list_.py b/TermTk/TTkWidgets/list_.py index 4442349e..cb3aa97f 100644 --- a/TermTk/TTkWidgets/list_.py +++ b/TermTk/TTkWidgets/list_.py @@ -35,7 +35,7 @@ class TTkList(TTkAbstractScrollArea): __slots__ = tuple( ['_listView'] + (_forwardedSignals:=[ # Forwarded Signals From TTkTable - 'itemClicked', 'textClicked']) + + 'itemClicked', 'textClicked', 'searchModified']) + (_forwardedMethods:=[ # Forwarded Methods From TTkTable 'items', 'dragDropMode', 'setDragDropMode', @@ -43,6 +43,7 @@ class TTkList(TTkAbstractScrollArea): 'indexOf', 'itemAt', 'moveItem', 'removeAt', 'removeItem', 'removeItems', 'selectionMode', 'setSelectionMode', 'selectedItems', 'selectedLabels', + 'search', 'setSearch', 'searchVisibility', 'setSearchVisibility', 'setCurrentRow', 'setCurrentItem']) ) _forwardWidget = TTkListWidget diff --git a/TermTk/TTkWidgets/listwidget.py b/TermTk/TTkWidgets/listwidget.py index 393f297c..8d257206 100644 --- a/TermTk/TTkWidgets/listwidget.py +++ b/TermTk/TTkWidgets/listwidget.py @@ -51,6 +51,7 @@ class TTkAbstractListItem(TTkWidget): } __slots__ = ('_text', '_selected', '_highlighted', '_data', + '_lowerText', '_quickVisible', 'listItemClicked') def __init__(self, *, text='', data=None, **kwargs) -> None: self.listItemClicked = pyTTkSignal(TTkAbstractListItem) @@ -59,6 +60,8 @@ def __init__(self, *, text='', data=None, **kwargs) -> None: self._highlighted = False self._text = TTkString(text) + self._lowerText = str(self._text).lower() + self._quickVisible = True self._data = data super().__init__(**kwargs) @@ -70,6 +73,7 @@ def text(self): def setText(self, text): self._text = TTkString(text) + self._lowerText = str(self._text).lower() self.update() def data(self): @@ -96,6 +100,12 @@ def _setHighlighted(self, highlighted): if self._highlighted == highlighted: return self._highlighted = highlighted self.update() + + def geometry(self): + if self._quickVisible: + return super().geometry() + else: + return 0,0,0,0 def paintEvent(self, canvas): color = (style:=self.currentStyle())['color'] @@ -105,9 +115,9 @@ def paintEvent(self, canvas): color = color+self.style()['selected']['color'] if style==self.style()['hover']: color = color+self.style()['hover']['color'] - + w = self.width() - + canvas.drawTTkString(pos=(0,0), width=w, color=color ,text=self._text) class TTkListWidget(TTkAbstractScrollView): @@ -159,10 +169,16 @@ class TTkListWidget(TTkAbstractScrollView): :param text: the text of the item selected :type text: str ''' + searchModified:pyTTkSignal + ''' + This signal is emitted whenever the search text is modified. + + :param text: the search text + :type text: str + ''' classStyle = { - 'default': {'searchColor': TTkColor.fg("#FFFF00") + TTkColor.UNDERLINE}, - } + 'default':{'searchColor': TTkColor.fg("#FFFF00") + TTkColor.UNDERLINE}} @dataclass(frozen=True) class _DropListData: @@ -174,7 +190,7 @@ class _DropListData: '_dragPos', '_dndMode', '_searchText', '_showSearch', # Signals - 'itemClicked', 'textClicked','searchModified') + 'itemClicked', 'textClicked', 'searchModified') def __init__(self, *, items:list[str]=[], selectionMode:int=TTkK.SelectionMode.SingleSelection, @@ -205,7 +221,7 @@ def __init__(self, *, self._dragPos = None self._dndMode = dragDropMode self._searchText:str = '' - self._showSearch:bool = showSearch + self._searchVisibility:bool = showSearch # Init Super super().__init__(**kwargs) self.addItems(items) @@ -243,30 +259,39 @@ def _labelSelectedHandler(self, label:TTkAbstractListItem): @pyTTkSlot(str) def _searchModifiedHandler(self, text:str='s') -> None: - if self._showSearch and self._searchText: + if self._searchVisibility and self._searchText: self.setPadding(1,0,0,0) else: self.setPadding(0,0,0,0) if self._searchText: text = self._searchText.lower() - self._filteredItems = [i for i in self._items if text in str(i.text()).lower()] + self._filteredItems = [i for i in self._items if text in i._lowerText] for item in self._items: - item.setVisible(text in str(item.text()).lower()) + item._quickVisible = text in item._lowerText else: self._filteredItems = self._items for item in self._items: item.setVisible(True) self._placeItems() - - def showSearch(self) -> bool: - '''showSearch''' - return self._showSearch - def setShowSearch(self, showSearch:bool) -> None: - '''setShowSearch''' - self._showSearch = showSearch + def search(self) -> str: + '''search''' + return self._searchText + + def setSearch(self, search:str) -> None: + '''setSearch''' + self._searchText = search + self.searchModified.emit(search) + + def searchVisibility(self) -> bool: + '''searchVisibility''' + return self._searchVisibility + + def setSearchVisibility(self, visibility:bool) -> None: + '''setSearchVisibility''' + self._searchVisibility = visibility def dragDropMode(self): '''dragDropMode''' @@ -323,8 +348,6 @@ def _placeItems(self): for item in self._items: if item in self._filteredItems: minw = max(minw,item.minimumWidth()) - else: - item.setGeometry(0,0,0,0) for y,item in enumerate(self._filteredItems): item.setGeometry(0,y,minw,1) self.viewChanged.emit() @@ -492,7 +515,7 @@ def dropEvent(self, evt:TTkDnDEvent) -> bool: return False def keyEvent(self, evt:TTkKeyEvent) -> bool: - if not self._highlighted: return False + # if not self._highlighted: return False if ( not self._searchText and evt.type == TTkK.Character and evt.key==" " ) or \ ( evt.type == TTkK.SpecialKey and evt.key == TTkK.Key_Enter ): if self._highlighted: @@ -505,17 +528,22 @@ def keyEvent(self, evt:TTkKeyEvent) -> bool: elif evt.type == TTkK.SpecialKey: if evt.key == TTkK.Key_Tab: return False - index = self._items.index(self._highlighted) + index = 0 + if self._highlighted: + self._highlighted._setHighlighted(False) + if self._highlighted not in self._filteredItems: + self._highlighted = self._filteredItems[0] + index = self._filteredItems.index(self._highlighted) offx,offy = self.getViewOffsets() h = self.height() if evt.key == TTkK.Key_Up: index = max(0, index-1) elif evt.key == TTkK.Key_Down: - index = min(len(self._items)-1, index+1) + index = min(len(self._filteredItems)-1, index+1) elif evt.key == TTkK.Key_PageUp: index = max(0, index-h) elif evt.key == TTkK.Key_PageDown: - index = min(len(self._items)-1, index+h) + index = min(len(self._filteredItems)-1, index+h) elif evt.key == TTkK.Key_Right: self.viewMoveTo(offx+1, offy) elif evt.key == TTkK.Key_Left: @@ -529,8 +557,7 @@ def keyEvent(self, evt:TTkKeyEvent) -> bool: self._searchText = self._searchText[:-1] self.update() self.searchModified.emit(self._searchText) - self._highlighted._setHighlighted(False) - self._highlighted = self._items[index] + self._highlighted = self._filteredItems[index] self._highlighted._setHighlighted(True) self._moveToHighlighted() else: @@ -562,7 +589,7 @@ def paintChildCanvas(self): canvas.drawText(pos=p2,text="╓─╼", color=TTkColor.fg("#FFFF00")+TTkColor.bg("#008855")) def paintEvent(self, canvas): - if self._showSearch and self._searchText: + if self._searchVisibility and self._searchText: w,h = self.size() if len(self._searchText) > w: text = f"...{self._searchText[-w-3:]}" diff --git a/tests/t.ui/test.ui.014.list.05.stress.py b/tests/t.ui/test.ui.014.list.05.stress.py new file mode 100755 index 00000000..57ce8b20 --- /dev/null +++ b/tests/t.ui/test.ui.014.list.05.stress.py @@ -0,0 +1,32 @@ +#!/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 + +root = ttk.TTk(title="pyTermTk List Demo", mouseTrack=True) +ttk.TTkComboBox(parent=root, size=(30,1), list=['abap', 'abl', 'abnf', 'aconf', 'actionscript', 'actionscript3', 'ada', 'ada2005', 'ada95', 'adl', 'agda', 'aheui', 'ahk', 'alloy', 'ambienttalk', 'ambienttalk/2', 'amdgpu', 'ampl', 'androidbp', 'ansys', 'antlr', 'antlr-actionscript', 'antlr-as', 'antlr-c#', 'antlr-cpp', 'antlr-csharp', 'antlr-java', 'antlr-objc', 'antlr-perl', 'antlr-python', 'antlr-rb', 'antlr-ruby', 'apache', 'apacheconf', 'apdl', 'apl', 'applescript', 'arduino', 'arexx', 'arrow', 'art', 'arturo', 'as', 'as3', 'asc', 'asm', 'asn1', 'aspectj', 'aspx-cs', 'aspx-vb', 'asy', 'asymptote', 'at', 'augeas', 'autohotkey', 'autoit', 'awk', 'b3d', 'bare', 'basemake', 'bash', 'basic', 'bat', 'batch', 'bazel', 'bbcbasic', 'bbcode', 'bc', 'bdd', 'be', 'befunge', 'berry', 'bf', 'bib', 'bibtex', 'blitzbasic', 'blitzmax', 'blueprint', 'bmax', 'bnf', 'boa', 'boo', 'boogie', 'bp', 'bplus', 'bqn', 'brainfuck', 'bro', 'bsdmake', 'bst', 'bst-pybtex', 'bugs', 'c', 'c#', 'c++', 'c++-objdumb', 'c-objdump', 'ca65', 'cadl', 'camkes', 'capdl', 'capnp', 'carbon', 'cbmbas', 'cddl', 'ceylon', 'cf3', 'cfc', 'cfengine3', 'cfg', 'cfm', 'cfs', 'chai', 'chaiscript', 'chapel', 'charmci', 'cheetah', 'chpl', 'cirru', 'cl', 'clay', 'clean', 'clipper', 'clj', 'cljs', 'clojure', 'clojurescript', 'cmake', 'cobol', 'cobolfree', 'coffee', 'coffee-script', 'coffeescript', 'comal', 'comal80', 'common-lisp', 'componentpascal', 'console', 'control', 'coq', 'cp', 'cplint', 'cpp', 'cpp-objdump', 'cpsa', 'cr', 'crmsh', 'croc', 'cry', 'cryptol', 'crystal', 'cs', 'csh', 'csharp', 'csound', 'csound-csd', 'csound-document', 'csound-orc', 'csound-sco', 'csound-score', 'css', 'css+django', 'css+erb', 'css+genshi', 'css+genshitext', 'css+jinja', 'css+lasso', 'css+mako', 'css+mozpreproc', 'css+myghty', 'css+php', 'css+ruby', 'css+smarty', 'css+ul4', 'cu', 'cucumber', 'cuda', 'cxx-objdump', 'cypher', 'cython', 'd', 'd-objdump', 'dart', 'dasm16', 'dax', 'debcontrol', 'debsources', 'delphi', 'desktop', 'devicetree', 'dg', 'diff', 'django', 'dmesg', 'do', 'docker', 'dockerfile', 'dosbatch', 'doscon', 'dosini', 'dot', 'dpatch', 'dtd', 'dts', 'duby', 'duel', 'dylan', 'dylan-console', 'dylan-lid', 'dylan-repl', 'earl-grey', 'earlgrey', 'easytrieve', 'ebnf', 'ec', 'ecl', 'eg', 'eiffel', 'elisp', 'elixir', 'elm', 'elpi', 'emacs', 'emacs-lisp', 'email', 'eml', 'erb', 'erl', 'erlang', 'evoque', 'ex', 'execline', 'exs', 'extempore', 'ezhil', 'f#', 'f90', 'factor', 'fan', 'fancy', 'fc', 'felix', 'fennel', 'fif', 'fift', 'fish', 'fishshell', 'flatline', 'flo', 'floscript', 'flx', 'fnl', 'forth', 'fortran', 'fortranfixed', 'foxpro', 'freefem', 'fsharp', 'fstar', 'func', 'futhark', 'fy', 'gap', 'gap-console', 'gap-repl', 'gas', 'gawk', 'gcode', 'gd', 'gdscript', 'genshi', 'genshitext', 'gherkin', 'glsl', 'gnuplot', 'go', 'golang', 'golo', 'gooddata-cl', 'gosu', 'graphql', 'graphviz', 'groff', 'groovy', 'gsed', 'gsql', 'gst', 'haml', 'handlebars', 'haskell', 'haxe', 'haxeml', 'hcl', 'hexdump', 'hlsl', 'hs', 'hsa', 'hsail', 'hspec', 'html', 'html+cheetah', 'html+django', 'html+erb', 'html+evoque', 'html+genshi', 'html+handlebars', 'html+jinja', 'html+kid', 'html+lasso', 'html+mako', 'html+myghty', 'html+ng2', 'html+php', 'html+ruby', 'html+smarty', 'html+spitfire', 'html+twig', 'html+ul4', 'html+velocity', 'htmlcheetah', 'htmldjango', 'http', 'hx', 'hxml', 'hxsl', 'hy', 'hybris', 'hylang', 'i6', 'i6t', 'i7', 'icon', 'idl', 'idl4', 'idr', 'idris', 'iex', 'igor', 'igorpro', 'ik', 'inform6', 'inform7', 'ini', 'io', 'ioke', 'irb', 'irc', 'isabelle', 'j', 'jade', 'jags', 'janet', 'jasmin', 'jasminxt', 'java', 'javascript', 'javascript+cheetah', 'javascript+django', 'javascript+erb', 'javascript+genshi', 'javascript+genshitext', 'javascript+jinja', 'javascript+lasso', 'javascript+mako', 'javascript+mozpreproc', 'javascript+myghty', 'javascript+php', 'javascript+ruby', 'javascript+smarty', 'javascript+spitfire', 'jbst', 'jcl', 'jinja', 'jl', 'jlcon', 'jmespath', 'jp', 'jproperties', 'js', 'js+cheetah', 'js+django', 'js+erb', 'js+genshi', 'js+genshitext', 'js+jinja', 'js+lasso', 'js+mako', 'js+myghty', 'js+php', 'js+ruby', 'js+smarty', 'js+spitfire', 'js+ul4', 'jsgf', 'jslt', 'json', 'json-ld', 'json-object', 'jsonld', 'jsonml+bst', 'jsonnet', 'jsp', 'jsx', 'julia', 'julia-repl', 'juttle', 'k', 'kal', 'kconfig', 'kernel-config', 'kid', 'kmsg', 'koka', 'kotlin', 'kql', 'ksh', 'kuin', 'kusto', 'lagda', 'lasso', 'lassoscript', 'latex', 'lcry', 'lcryptol', 'ldapconf', 'ldaprc', 'ldif', 'lean', 'lean3', 'lean4', 'less', 'lhaskell', 'lhs', 'lid', 'lidr', 'lidris', 'lighttpd', 'lighty', 'lilypond', 'limbo', 'linux-config', 'linuxconfig', 'liquid', 'lisp', 'literate-agda', 'literate-cryptol', 'literate-haskell', 'literate-idris', 'live-script', 'livescript', 'llvm', 'llvm-mir', 'llvm-mir-body', 'lobas', 'logos', 'logtalk', 'lsl', 'lua', 'luau', 'm2', 'macaulay2', 'macsyma', 'make', 'makefile', 'mako', 'man', 'maql', 'markdown', 'mask', 'mason', 'mathematica', 'matlab', 'matlabsession', 'mawk', 'maxima', 'mcf', 'mcfunction', 'mcschema', 'md', 'mediawiki', 'menuconfig', 'meson', 'meson.build', 'mf', 'mime', 'minid', 'miniscript', 'mips', 'mma', 'modelica', 'modula2', 'moin', 'mojo', 'monkey', 'monte', 'moo', 'moocode', 'moon', 'moonscript', 'mosel', 'mozhashpreproc', 'mozpercentpreproc', 'mq4', 'mq5', 'mql', 'mql4', 'mql5', 'ms', 'msc', 'mscgen', 'mupad', 'mxml', 'myghty', 'mysql', 'nasm', 'nawk', 'nb', 'ncl', 'nemerle', 'nesc', 'nestedtext', 'newlisp', 'newspeak', 'ng2', 'nginx', 'nim', 'nimrod', 'nit', 'nix', 'nixos', 'nodejsrepl', 'notmuch', 'nroff', 'nsh', 'nsi', 'nsis', 'nt', 'numpy', 'nusmv', 'obj-c', 'obj-c++', 'obj-j', 'objc', 'objc++', 'objdump', 'objdump-nasm', 'objective-c', 'objective-c++', 'objective-j', 'objectivec', 'objectivec++', 'objectivej', 'objectpascal', 'objj', 'ocaml', 'octave', 'odin', 'omg-idl', 'oobas', 'ooc', 'opa', 'openbugs', 'openedge', 'openrc', 'openscad', 'org', 'org-mode', 'orgmode', 'output', 'pacmanconf', 'pan', 'parasail', 'pas', 'pascal', 'pawn', 'pcmk', 'peg', 'pem', 'perl', 'perl6', 'phix', 'php', 'php3', 'php4', 'php5', 'pig', 'pike', 'pkgconfig', 'pl', 'pl6', 'plpgsql', 'po', 'pointless', 'pony', 'portugol', 'posh', 'postgres', 'postgres-console', 'postgres-explain', 'postgresql', 'postgresql-console', 'postscr', 'postscript', 'pot', 'pov', 'powershell', 'praat', 'procfile', 'progress', 'prolog', 'promela', 'promql', 'properties', 'proto', 'protobuf', 'prql', 'ps1', 'ps1con', 'psm1', 'psql', 'psysh', 'ptx', 'pug', 'puppet', 'pwsh', 'pwsh-session', 'py', 'py+ul4', 'py2', 'py2tb', 'py3', 'py3tb', 'pycon', 'pypy', 'pypylog', 'pyrex', 'pytb', 'python', 'python-console', 'python2', 'python3', 'pyx', 'q', 'qbasic', 'qbs', 'qlik', 'qlikscript', 'qliksense', 'qlikview', 'qml', 'qvt', 'qvto', 'r', 'racket', 'ragel', 'ragel-c', 'ragel-cpp', 'ragel-d', 'ragel-em', 'ragel-java', 'ragel-objc', 'ragel-rb', 'ragel-ruby', 'raku', 'rb', 'rbcon', 'rconsole', 'rd', 'react', 'reason', 'reasonml', 'rebol', 'red', 'red/system', 'redcode', 'registry', 'resource', 'resourcebundle', 'rest', 'restructuredtext', 'rexx', 'rhtml', 'ride', 'rita', 'rkt', 'rnc', 'rng-compact', 'roboconf-graph', 'roboconf-instances', 'robotframework', 'rout', 'rql', 'rs', 'rsl', 'rst', 'rts', 'ruby', 'rust', 's', 'sage', 'salt', 'sarl', 'sas', 'sass', 'savi', 'sbatch', 'sc', 'scala', 'scaml', 'scd', 'scdoc', 'scheme', 'scilab', 'scm', 'scss', 'sed', 'sgf', 'sh', 'shell', 'shell-session', 'shen', 'shex', 'shexc', 'sieve', 'silver', 'singularity', 'slash', 'slim', 'sls', 'slurm', 'smali', 'smalltalk', 'smarty', 'smithy', 'sml', 'snbt', 'snobol', 'snowball', 'sobas', 'solidity', 'soong', 'sophia', 'sources.list', 'sourceslist', 'sp', 'sparql', 'spec', 'spice', 'spicelang', 'spitfire', 'splus', 'sql', 'sql+jinja', 'sqlite3', 'squeak', 'squid', 'squid.conf', 'squidconf', 'srcinfo', 'ssed', 'ssp', 'st', 'stan', 'starlark', 'stata', 'supercollider', 'sv', 'swift', 'swig', 'systemd', 'systemverilog', 't-sql', 'tact', 'tads3', 'tal', 'tap', 'tasm', 'tcl', 'tcsh', 'tcshcon', 'tea', 'teal', 'teraterm', 'teratermmacro', 'termcap', 'terminfo', 'terraform', 'tex', 'text', 'tf', 'thingsdb', 'thrift', 'ti', 'tid', 'tlb', 'tls', 'tnt', 'todotxt', 'toml', 'trac-wiki', 'trafficscript', 'treetop', 'ts', 'tsql', 'ttl', 'turtle', 'twig', 'typescript', 'typoscript', 'typoscriptcssdata', 'typoscripthtmldata', 'typst', 'ucode', 'udiff', 'ul4', 'unicon', 'unixconfig', 'urbiscript', 'urlencoded', 'usd', 'usda', 'uxntal', 'v', 'vala', 'vapi', 'vb.net', 'vbnet', 'vbscript', 'vcl', 'vclsnippet', 'vclsnippets', 'vctreestatus', 'velocity', 'verifpal', 'verilog', 'vfp', 'vgl', 'vhdl', 'vim', 'visual-basic', 'visualbasic', 'visualprolog', 'visualprologgrammar', 'vyper', 'wast', 'wat', 'wdiff', 'webidl', 'wgsl', 'whiley', 'wikitext', 'winbatch', 'winbugs', 'wowtoc', 'wren', 'x++', 'x10', 'xbase', 'xml', 'xml+cheetah', 'xml+django', 'xml+erb', 'xml+evoque', 'xml+genshi', 'xml+jinja', 'xml+kid', 'xml+lasso', 'xml+mako', 'xml+myghty', 'xml+php', 'xml+ruby', 'xml+smarty', 'xml+spitfire', 'xml+ul4', 'xml+velocity', 'xorg.conf', 'xpp', 'xq', 'xql', 'xqm', 'xquery', 'xqy', 'xslt', 'xten', 'xtend', 'xul+mozpreproc', 'yaml', 'yaml+jinja', 'yang', 'yar', 'yara', 'zeek', 'zephir', 'zig', 'zone', 'zsh', '🔥']) +root.mainloop() From f8d8937a6e3d47ae97203debdc6ee0c68e581bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Tue, 7 Jan 2025 16:58:24 +0000 Subject: [PATCH 5/6] Improved the combobox with the search text displayed in the list border --- TermTk/TTkWidgets/combobox.py | 56 ++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/TermTk/TTkWidgets/combobox.py b/TermTk/TTkWidgets/combobox.py index 98b41a1d..a971e0ac 100644 --- a/TermTk/TTkWidgets/combobox.py +++ b/TermTk/TTkWidgets/combobox.py @@ -39,6 +39,41 @@ from TermTk.TTkWidgets.lineedit import TTkLineEdit from TermTk.TTkWidgets.resizableframe import TTkResizableFrame + +class _TTkComboBoxPopup(TTkResizableFrame): + classStyle = TTkResizableFrame.classStyle + classStyle['default'] |= {'searchColor': TTkColor.fg("#FFFF00")} + + __slots__ = ('_list', + #exportedMethods + 'setCurrentRow', + #exportedSignals + 'textClicked') + def __init__(self, *, items:list[str], **kwargs) -> None: + super().__init__(**kwargs|{'layout':TTkGridLayout()}) + self._list:TTkList = TTkList(parent=self, showSearch=False) + self._list.addItems(items) + self._list.searchModified.connect(self.update) + + self.textClicked = self._list.textClicked + self.setCurrentRow = self._list.setCurrentRow + + # def setFocus(self) -> None: + # self._list.viewport().setFocus() + + def keyEvent(self, evt:TTkKeyEvent) -> bool: + return self._list.viewport().keyEvent(evt) + + def paintEvent(self, canvas:TTkCanvas) -> None: + super().paintEvent(canvas) + if text := self._list.search(): + w = self.width()-6 + if len(text) > w: + text = f"...{text[w-3:]}" + color = self.currentStyle()['searchColor'] + canvas.drawText(pos=(1,0), text=f"╼ {text} ╾") + canvas.drawText(pos=(3,0), text=text,color=color) + class TTkComboBox(TTkContainer): ''' TTkComboBox: @@ -333,16 +368,23 @@ def _pressEvent(self) -> bool: if frameHeight > 20: frameHeight = 20 if frameWidth < 20: frameWidth = 20 - self._popupFrame = TTkResizableFrame(layout=TTkGridLayout(), size=(frameWidth,frameHeight)) + self._popupFrame = _TTkComboBoxPopup(items=self._list, size=(frameWidth,frameHeight)) TTkHelper.overlay(self, self._popupFrame, 0, 0) - listw = TTkList(parent=self._popupFrame) - # TTkLog.debug(f"{self._list}") - listw.addItems(self._list) if self._id != -1: - listw.setCurrentRow(self._id) - listw.textClicked.connect(self._callback) - listw.viewport().setFocus() + self._popupFrame.setCurrentRow(self._id) + self._popupFrame.textClicked.connect(self._callback) self.update() + + # self._popupFrame = TTkResizableFrame(layout=TTkGridLayout(), size=(frameWidth,frameHeight)) + # TTkHelper.overlay(self, self._popupFrame, 0, 0) + # listw = TTkList(parent=self._popupFrame) + # # TTkLog.debug(f"{self._list}") + # listw.addItems(self._list) + # if self._id != -1: + # listw.setCurrentRow(self._id) + # listw.textClicked.connect(self._callback) + # listw.viewport().setFocus() + # self.update() return True def wheelEvent(self, evt:TTkMouseEvent) -> bool: From 0877efc1df1198649a5595b444b5d34f1017240e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eugenio=20Parodi=20=F0=9F=8C=B6=EF=B8=8F?= Date: Tue, 7 Jan 2025 16:58:53 +0000 Subject: [PATCH 6/6] improved the formwidget demo --- demo/showcase/formwidgets02.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/demo/showcase/formwidgets02.py b/demo/showcase/formwidgets02.py index 9aa66756..120c0db0 100755 --- a/demo/showcase/formwidgets02.py +++ b/demo/showcase/formwidgets02.py @@ -137,9 +137,12 @@ def demoFormWidgets(root=None): def main(): parser = argparse.ArgumentParser() parser.add_argument('-f', help='Full Screen', action='store_true') + parser.add_argument('-t', help='Track Mouse', action='store_true') args = parser.parse_args() - root = ttk.TTk() + mouseTrack = args.t + + root = ttk.TTk(title="pyTermTk Form Demo", mouseTrack=mouseTrack) if args.f: rootForm = root root.setLayout(ttk.TTkGridLayout())