Source code for TermTk.TTkWidgets.listwidget

# MIT License
#
# Copyright (c) 2021 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# 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.

__all__ = ['TTkAbstractListItem', 'TTkListWidget', 'TTkAbstractListItemType']

from dataclasses import dataclass
from typing import Union, Optional, List, Any

from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.log import TTkLog
from TermTk.TTkCore.signal import pyTTkSlot, pyTTkSignal
from TermTk.TTkCore.color import TTkColor
from TermTk.TTkCore.canvas import TTkCanvas
from TermTk.TTkCore.string import TTkString,TTkStringType
from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
from TermTk.TTkGui.drag import TTkDrag, TTkDnDEvent

from TermTk.TTkWidgets.widget import TTkWidget
from TermTk.TTkAbstract.abstractscrollview import TTkAbstractScrollView

[docs] class TTkAbstractListItem(TTkWidget): '''TTkAbstractListItem: Base class for items in a :py:class:`TTkListWidget`. This widget represents a single selectable item that can be highlighted, selected, and clicked. It supports custom styling for different states (default, highlighted, selected, hover, disabled). :: ┌────────────────────┐ │ Normal Item │ Default state │ Highlighted Item │ Highlighted (navigation) │ Selected Item │ Selected by user └────────────────────┘ ''' classStyle = TTkWidget.classStyle | { 'default': {'color': TTkColor.RST}, 'highlighted': {'color': TTkColor.bg('#008855')+TTkColor.UNDERLINE}, 'hover': {'color': TTkColor.bg('#0088FF')}, 'selected': {'color': TTkColor.bg('#0055FF')}, 'clicked': {'color': TTkColor.fg('#FFFF00')}, 'disabled': {'color': TTkColor.fg('#888888')}, } __slots__ = ('_text', '_selected', '_highlighted', '_data', '_lowerText', '_quickVisible', 'listItemClicked') def __init__(self, *, text:TTkStringType='', data=None, **kwargs) -> None: ''' :param text: The display text for this item, defaults to '' :type text: str or :py:class:`TTkString`, optional :param data: Optional user data associated with this item, defaults to None :type data: Any, optional ''' self.listItemClicked = pyTTkSignal(TTkAbstractListItem) ''' This signal is emitted when the item is clicked. :param item: The item that was clicked :type item: :py:class:`TTkAbstractListItem` ''' self._selected = False self._highlighted = False if isinstance(text,str): self._text = TTkString(text) else: self._text = text self._lowerText = str(self._text).lower() self._quickVisible = True self._data = data super().__init__(**kwargs) self.setFocusPolicy(TTkK.ParentFocus)
[docs] def text(self) -> TTkString: ''' Returns the item's display text. :return: The text displayed by this item :rtype: :py:class:`TTkString` ''' return self._text
[docs] def setText(self, text: str) -> None: ''' Sets the item's display text. :param text: The new text to display :type text: str or :py:class:`TTkString` ''' self._text = TTkString(text) self._lowerText = str(self._text).lower() self.update()
[docs] def data(self) -> Any: ''' Returns the user data associated with this item. :return: The custom data object :rtype: Any ''' return self._data
[docs] def setData(self, data: Any) -> None: ''' Sets the user data associated with this item. :param data: The custom data object to store :type data: Any ''' if self._data == data: return self._data = data self.update()
def mousePressEvent(self, evt: TTkMouseEvent) -> bool: self.listItemClicked.emit(self) return True def _setSelected(self, selected: bool) -> None: ''' Internal method to set the selected state. :param selected: True to select, False to deselect :type selected: bool ''' if self._selected == selected: return self._selected = selected self._highlighted = not selected self.update() def _setHighlighted(self, highlighted: bool) -> None: ''' Internal method to set the highlighted state. :param highlighted: True to highlight, False to unhighlight :type highlighted: bool ''' 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: TTkCanvas) -> None: color = (style:=self.currentStyle())['color'] if self._highlighted: color = color+self.style()['highlighted']['color'] if self._selected: 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)
TTkAbstractListItemType = Union[TTkAbstractListItem, Any]
[docs] class TTkListWidget(TTkAbstractScrollView): '''TTkListWidget: A widget that displays a scrollable list of selectable items with optional search functionality. This widget supports single/multiple selection modes, drag-and-drop reordering, keyboard navigation, and incremental search. Items can be strings or custom :py:class:`TTkAbstractListItem` widgets. :: ╔════════════════════════════════╗ ║Search: te_ ▲║ ← Search bar (optional) ║S-0) --Zero-3- officia ▓║ ║S-1) ad ipsum ┊║ ║S-2) irure nisi ┊║ ← Scrollable items ║S-3) minim --Zero-3- ┊║ ║S-4) ea sunt ┊║ ║S-5) qui mollit ┊║ ║S-6) magna sunt ┊║ ║S-7) sunt officia ▼║ ╚════════════════════════════════╝ Demo: `list.py <https://github.com/ceccopierangiolieugenio/pyTermTk/blob/main/demo/showcase/list.py>`_ (`online <https://ceccopierangiolieugenio.github.io/pyTermTk-Docs/sandbox/sandbox.html?filePath=demo/showcase/list.py>`__) .. code-block:: python import TermTk as ttk root = ttk.TTk(layout=ttk.TTkGridLayout(), mouseTrack=True) # Simple string list l1 = ttk.TTkList(parent=root, items=[123, 456, 789]) id1 = l1.indexOf(456) l1.setCurrentRow(id1) # List with many items (scrollable) ttk.TTkList(parent=root, items=[f"Item 0x{i:03X}" for i in range(100)]) # Multi-selection list with drag-drop ttkList = ttk.TTkList( parent=root, selectionMode=ttk.TTkK.SelectionMode.MultiSelection, dragDropMode=ttk.TTkK.DragDropMode.AllowDragDrop ) ttkList.addItems([f"Item 0x{i:04X}" for i in range(50)]) root.layout().addWidget(ttk.TTkLogViewer(),1,0,1,3) # Handle selection @ttk.pyTTkSlot(str) def on_item_clicked(text): ttk.TTkLog.debug(f"Clicked: {text}") ttkList.textClicked.connect(on_item_clicked) root.mainloop() **Features:** - Single or multiple selection modes - Keyboard navigation (arrows, page up/down, home/end) - Incremental search by typing - Drag-and-drop reordering (optional) - Custom item widgets via :py:class:`TTkAbstractListItem` - Signals for item selection and search events ''' @property def itemClicked(self) -> pyTTkSignal: ''' This signal is emitted whenever an item is clicked. :param item: the item selected :type item: :py:class:`TTkAbstractListItem` ''' return self._itemClicked @property def textClicked(self) -> pyTTkSignal: ''' This signal is emitted whenever an item is clicked. :param text: the text of the item selected :type text: str ''' return self._textClicked @property def searchModified(self) -> pyTTkSignal: ''' This signal is emitted whenever the search text is modified. :param text: the search text :type text: str ''' return self._searchModified classStyle = { 'default':{'searchColor': TTkColor.fg("#FFFF00") + TTkColor.UNDERLINE}} @dataclass(frozen=True) class _DropListData: widget: TTkAbstractScrollView items: list __slots__ = ('_selectedItems', '_selectionMode', '_highlighted', '_items', '_filteredItems', '_dragPos', '_dndMode', '_searchText', '_showSearch', # Signals '_itemClicked', '_textClicked', '_searchModified') _items:List[TTkAbstractListItem] _showSearch:bool _highlighted:Optional[TTkAbstractListItem] _selectedItems:List[TTkAbstractListItem] _filteredItems:List[TTkAbstractListItem] def __init__(self, *, items:List[TTkAbstractListItemType]=[], selectionMode:TTkK.SelectionMode=TTkK.SelectionMode.SingleSelection, dragDropMode:TTkK.DragDropMode=TTkK.DragDropMode.NoDragDrop, showSearch:bool=True, **kwargs) -> None: ''' :param items: Initial list of items (Any or :py:class:`TTkAbstractListItem` objects), defaults to [] :type items: list, optional :param selectionMode: Selection behavior (:py:class:`TTkK.SelectionMode.SingleSelection` or :py:class:`TTkK.SelectionMode.MultiSelection`), defaults to :py:class:`TTkK.SelectionMode.SingleSelection` :type selectionMode: :py:class:`TTkK.SelectionMode`, optional :param dragDropMode: Drag and drop behavior (NoDragDrop, InternalMove, DragOnly, DropOnly, DragDrop), defaults to NoDragDrop :type dragDropMode: :py:class:`TTkK.DragDropMode`, optional :param showSearch: Whether to show the search hint at the top, 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._filteredItems = self._items self._highlighted = None self._dragPos = None self._dndMode = dragDropMode self._searchText:str = '' self._showSearch:bool = showSearch # Init Super super().__init__(**kwargs) self.addItemsAt(items=items, pos=0) self.viewChanged.connect(self._viewChangedHandler) self.setFocusPolicy(TTkK.ClickFocus | TTkK.TabFocus) self.searchModified.connect(self._searchModifiedHandler) @pyTTkSlot() def _viewChangedHandler(self): x,y = self.getViewOffsets() self.layout().setOffset(-x,-y) @pyTTkSlot(TTkAbstractListItem) def _labelSelectedHandler(self, label:TTkAbstractListItem): if self._selectionMode == TTkK.SingleSelection: for item in self._selectedItems: item._setSelected(False) item._setHighlighted(False) self._selectedItems = [label] label._setSelected(True) elif self._selectionMode == TTkK.MultiSelection: for item in self._selectedItems: item._setHighlighted(False) label._setSelected(not label._selected) if label._selected: self._selectedItems.append(label) else: self._selectedItems.remove(label) if self._highlighted: self._highlighted._setHighlighted(False) label._setHighlighted(True) self._highlighted = label self.itemClicked.emit(label) self.textClicked.emit(label.text()) @pyTTkSlot(str) def _searchModifiedHandler(self) -> 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 i._lowerText] for item in self._items: item._quickVisible = text in item._lowerText else: self._filteredItems = self._items for item in self._items: item._quickVisible = True item.setVisible(True) self._placeItems()
[docs] def search(self) -> str: ''' Returns the current search text. :return: The active search filter string :rtype: str ''' return self._searchText
[docs] def setSearch(self, search:str) -> None: ''' Sets the search text to filter items. :param search: The search string to filter by :type search: str ''' self._searchText = search self.searchModified.emit(search)
[docs] def searchVisibility(self) -> bool: ''' Returns whether the search hint is visible. :return: True if search hint is shown :rtype: bool ''' return self._showSearch
[docs] def setSearchVisibility(self, visibility:bool) -> None: ''' Sets the visibility of the search hint at the top of the list. :param visibility: True to show search hint, False to hide :type visibility: bool ''' self._showSearch = visibility
[docs] def dragDropMode(self) -> TTkK.DragDropMode: ''' Returns the current drag-drop mode. :return: The drag-drop behavior setting :rtype: :py:class:`TTkK.DragDropMode` ''' return self._dndMode
[docs] def setDragDropMode(self, dndMode:TTkK.DragDropMode) -> None: ''' Sets the drag-drop mode for this list. :param dndMode: The new drag-drop behavior :type dndMode: :py:class:`TTkK.DragDropMode` ''' self._dndMode = dndMode
[docs] def selectionMode(self) -> TTkK.SelectionMode: ''' Returns the current selection mode. :return: The selection behavior setting :rtype: :py:class:`TTkK.SelectionMode` ''' return self._selectionMode
[docs] def setSelectionMode(self, mode:TTkK.SelectionMode) -> None: ''' Sets the selection mode for this list. :param mode: The new selection behavior (SingleSelection or MultiSelection) :type mode: :py:class:`TTkK.SelectionMode` ''' self._selectionMode = mode
[docs] def selectedItems(self) -> List[TTkAbstractListItem]: ''' Returns the list of currently selected items. :return: List of selected item widgets :rtype: list[:py:class:`TTkAbstractListItem`] ''' return self._selectedItems
[docs] def selectedLabels(self) -> List[str]: ''' Returns the text of all selected items. :return: List of selected item texts :rtype: list[str] ''' return [i.text() for i in self._selectedItems]
[docs] def items(self) -> List[TTkAbstractListItem]: ''' Returns all items in the list. :return: Complete list of items :rtype: list[:py:class:`TTkAbstractListItem`] ''' return self._items
[docs] def filteredItems(self) -> List[TTkAbstractListItem]: ''' Returns items matching the current search filter. :return: Filtered list of visible items :rtype: list[:py:class:`TTkAbstractListItem`] ''' return self._filteredItems
def resizeEvent(self, w:int, h:int) -> None: maxw = 0 for item in self.layout().children(): maxw = max(maxw,item.minimumWidth()) maxw = max(self.width(),maxw) for item in self.layout().children(): x,y,_,h = item.geometry() item.setGeometry(x,y,maxw,h) TTkAbstractScrollView.resizeEvent(self, w, h)
[docs] def addItem(self, item:TTkAbstractListItemType, data:Any=None) -> None: ''' Appends a single item to the end of the list. :param item: The item to add (string or :py:class:`TTkAbstractListItem`) :type item: str or :py:class:`TTkAbstractListItem` :param data: Optional user data to associate with the item :type data: Any, optional ''' self.addItemAt(item, len(self._items), data)
[docs] def addItems(self, items:List[TTkAbstractListItemType]) -> None: ''' Appends multiple items to the end of the list. :param items: List of items to add (strings or :py:class:`TTkAbstractListItem` objects) :type items: list ''' self.addItemsAt(items=items, pos=len(self._items))
def _placeItems(self) -> None: ''' Internal method to position items in the layout. ''' minw = self.width() for item in self._items: if item in self._filteredItems: minw = max(minw,item.minimumWidth()) for y,item in enumerate(self._filteredItems): item.setGeometry(0,y,minw,1) self.viewChanged.emit() self.update()
[docs] def addItemAt(self, item:TTkAbstractListItemType, pos:int, data:Any=None) -> None: ''' Inserts a single item at the specified position. :param item: The item to insert (string or :py:class:`TTkAbstractListItem`) :type item: str or :py:class:`TTkAbstractListItem` :param pos: The index position to insert at :type pos: int :param data: Optional user data to associate with the item :type data: Any, optional ''' if isinstance(item, str) or isinstance(item, TTkString): item = TTkAbstractListItem(text=item, data=data) self.addItemsAt([item],pos)
[docs] def addItemsAt(self, items:List[TTkAbstractListItemType], pos:int) -> None: ''' Inserts multiple items at the specified position. :param items: List of items to insert (strings or :py:class:`TTkAbstractListItem` objects) :type items: list :param pos: The index position to insert at :type pos: int ''' items = [ _i if isinstance(_i, TTkAbstractListItem) else TTkAbstractListItem( text=TTkString(_i if isinstance(_i,TTkString) else str(_i)), data=_i) for _i in items ] for item in items: if not issubclass(type(item),TTkAbstractListItem): TTkLog.error(f"{item=} is not an TTkAbstractListItem") return for item in items: item.listItemClicked.connect(self._labelSelectedHandler) self._items[pos:pos] = items self.layout().addWidgets(items) self._placeItems()
[docs] def indexOf(self, item:TTkAbstractListItemType) -> int: ''' Returns the index of the given item. :param item: The item to find :type item: :py:class:`TTkAbstractListItem` or the data or the text to be searched :return: The index of the item, or -1 if not found :rtype: int ''' if isinstance(item, TTkAbstractListItem): return self._items.index(item) for i, it in enumerate(self._items): if it.data() == item or it.text() == item: return i return -1
[docs] def itemAt(self, pos:int) -> TTkAbstractListItem: ''' Returns the item at the specified index. :param pos: The index position :type pos: int :return: The item at that position :rtype: :py:class:`TTkAbstractListItem` ''' return self._items[pos]
[docs] def moveItem(self, fr:int, to:int) -> None: ''' Moves an item from one position to another. :param fr: The source index :type fr: int :param to: The destination index :type to: int ''' fr = max(min(fr,len(self._items)-1),0) to = max(min(to,len(self._items)-1),0) # Swap self._items[to] , self._items[fr] = self._items[fr] , self._items[to] self._placeItems()
[docs] def removeItem(self, item:TTkAbstractListItem) -> None: ''' Removes a single item from the list. :param item: The item to remove :type item: :py:class:`TTkAbstractListItem` ''' self.removeItems([item])
[docs] def removeItems(self, items:List[TTkAbstractListItem]) -> None: ''' Removes multiple items from the list. :param items: List of items to remove :type items: list[:py:class:`TTkAbstractListItem`] ''' self.layout().removeWidgets(items) for item in items.copy(): item.listItemClicked.disconnect(self._labelSelectedHandler) item._setSelected(False) item._setHighlighted(False) self._items.remove(item) if item in self._selectedItems: self._selectedItems.remove(item) if item == self._highlighted: self._highlighted = None self._placeItems()
[docs] def removeAt(self, pos:int) -> None: ''' Removes the item at the specified index. :param pos: The index of the item to remove :type pos: int ''' self.removeItem(self._items[pos])
[docs] def setCurrentRow(self, row:int) -> None: ''' Selects the item at the specified row. :param row: The row index to select :type row: int ''' if row<len(self._items): item = self._items[row] self.setCurrentItem(item)
[docs] def setCurrentItem(self, item:TTkAbstractListItem) -> None: ''' Selects the specified item and emits the itemClicked signal. :param item: The item to select :type item: :py:class:`TTkAbstractListItem` ''' item.listItemClicked.emit(item)
def _moveToHighlighted(self) -> None: ''' Internal method to scroll the view to show the highlighted item. ''' index = self._items.index(self._highlighted) h = self.height() offx,offy = self.getViewOffsets() if index >= h+offy-1: self.viewMoveTo(offx, index-h+1) 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 if not (items:=self._selectedItems.copy()): return True drag = TTkDrag() data =TTkListWidget._DropListData(widget=self,items=items) h = min(3,ih:=len(items)) + 2 + (1 if ih>3 else 0) w = min(20,iw:=max([it.text().termWidth() for it in items[:3]])) + 2 pm = TTkCanvas(width=w,height=h) for y,it in enumerate(items[:3],1): txt = it.text() if txt.termWidth() < 20: pm.drawText(pos=(1,y), text=it.text()) else: pm.drawText(pos=(1,y), text=it.text(), width=17) pm.drawText(pos=(18,y), text='...') if ih>3: pm.drawText(pos=(1,4), text='...') pm.drawBox(pos=(0,0),size=(w,h)) drag.setPixmap(pm) drag.setData(data) drag.exec() return True def dragEnterEvent(self, evt:TTkDnDEvent) -> bool: if not(self._dndMode & TTkK.DragDropMode.AllowDrop): return False if issubclass(type(evt.data()),TTkListWidget._DropListData): return self.dragMoveEvent(evt) return False def dragMoveEvent(self, evt:TTkDnDEvent) -> bool: offx,offy = self.getViewOffsets() y=min(evt.y+offy,len(self._items)) self._dragPos = (offx+evt.x, y) self.update() return True def dragLeaveEvent(self, evt:TTkDnDEvent) -> bool: self._dragPos = None self.update() return True def dropEvent(self, evt:TTkDnDEvent) -> bool: if not(self._dndMode & TTkK.DragDropMode.AllowDrop): return False 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']) 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 ( 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: self._highlighted.listItemClicked.emit(self._highlighted) elif evt.type == TTkK.Character: # Add this char to the search text self._searchText += evt.key self.update() self.searchModified.emit(self._searchText) elif ( evt.type == TTkK.SpecialKey and evt.key == TTkK.Key_Tab ): return False elif ( evt.type == TTkK.SpecialKey and evt.key in (TTkK.Key_Delete,TTkK.Key_Backspace) and self._searchText ): # Handle the backspace to remove the last char from the search text self._searchText = self._searchText[:-1] self.update() self.searchModified.emit(self._searchText) elif ( evt.type == TTkK.SpecialKey and self._filteredItems): # Handle the arrow/movement keys 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._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._filteredItems)-1, index+h) elif evt.key == TTkK.Key_Right: self.viewMoveTo(offx+1, offy) elif evt.key == TTkK.Key_Left: self.viewMoveTo(offx-1, offy) elif evt.key == TTkK.Key_Home: 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 = self._filteredItems[index] self._highlighted._setHighlighted(True) self._moveToHighlighted() else: return False return True def focusInEvent(self): if not self._items: return if not self._highlighted: self._highlighted = self._items[0] self._highlighted._setHighlighted(True) def focusOutEvent(self): if self._highlighted: self._highlighted._setHighlighted(False) self._dragPos = None # Stupid hack to paint on top of the child widgets def paintChildCanvas(self): super().paintChildCanvas() if self._dragPos: canvas = self.getCanvas() x,y = self._dragPos offx,offy = self.getViewOffsets() p1 = (0,y-offy-1) 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: TTkCanvas) -> None: if self._showSearch and self._searchText: w,h = self.size() color = self.currentStyle()['searchColor'] if len(self._searchText) > w: text = TTkString("≼",TTkColor.BG_BLUE+TTkColor.FG_CYAN)+TTkString(self._searchText[-w+1:],color) else: text = TTkString(self._searchText,color) canvas.drawTTkString(pos=(0,0),text=text, color=color, width=w)