From 4a380cf992cf6ea0d0ce5394730400916964693a Mon Sep 17 00:00:00 2001 From: Eugenio Parodi Date: Fri, 27 Sep 2024 11:41:33 +0100 Subject: [PATCH] Added Table sort --- TermTk/TTkCore/constant.py | 5 +- TermTk/TTkWidgets/TTkModelView/__init__.py | 1 + .../TTkWidgets/TTkModelView/tablemodelcsv.py | 62 ++-- .../TTkWidgets/TTkModelView/tablemodeljson.py | 0 .../TTkWidgets/TTkModelView/tablemodellist.py | 71 ++++ TermTk/TTkWidgets/TTkModelView/tablewidget.py | 118 ++++++- tests/t.ui/test.ui.032.table.06.py | 4 +- tests/t.ui/test.ui.032.table.07.py | 323 ++++++++++++++++++ tests/timeit/02.array.05.sort.py | 82 +++++ 9 files changed, 604 insertions(+), 62 deletions(-) create mode 100644 TermTk/TTkWidgets/TTkModelView/tablemodeljson.py create mode 100644 TermTk/TTkWidgets/TTkModelView/tablemodellist.py create mode 100755 tests/t.ui/test.ui.032.table.07.py create mode 100755 tests/timeit/02.array.05.sort.py diff --git a/TermTk/TTkCore/constant.py b/TermTk/TTkCore/constant.py index 30c0aabb..f064b0ab 100644 --- a/TermTk/TTkCore/constant.py +++ b/TermTk/TTkCore/constant.py @@ -154,9 +154,12 @@ class TTkConstant: DontShowIndicator = ChildIndicatorPolicy.DontShowIndicator DontShowIndicatorWhenChildless = ChildIndicatorPolicy.DontShowIndicatorWhenChildless - class SortOrder(int): + class SortOrder(): + '''This enum describes how the items in a widget are sorted.''' AscendingOrder = 0x00 + '''The items are sorted ascending e.g. starts with 'AAA' ends with 'ZZZ' in Latin-1 locales''' DescendingOrder = 0x01 + '''The items are sorted descending e.g. starts with 'ZZZ' ends with 'AAA' in Latin-1 locales''' AscendingOrder = SortOrder.AscendingOrder DescendingOrder = SortOrder.DescendingOrder diff --git a/TermTk/TTkWidgets/TTkModelView/__init__.py b/TermTk/TTkWidgets/TTkModelView/__init__.py index ad500912..83c11995 100644 --- a/TermTk/TTkWidgets/TTkModelView/__init__.py +++ b/TermTk/TTkWidgets/TTkModelView/__init__.py @@ -7,5 +7,6 @@ from .filetreewidgetitem import * from .table import * from .tablewidget import * from .tablewidgetitem import * +from .tablemodellist import * from .tablemodelcsv import * from .tablemodeljson import * diff --git a/TermTk/TTkWidgets/TTkModelView/tablemodelcsv.py b/TermTk/TTkWidgets/TTkModelView/tablemodelcsv.py index 46eb99bd..bb7ccd82 100644 --- a/TermTk/TTkWidgets/TTkModelView/tablemodelcsv.py +++ b/TermTk/TTkWidgets/TTkModelView/tablemodelcsv.py @@ -25,59 +25,37 @@ __all__=['TTkTableModelCSV'] import csv from TermTk.TTkCore.constant import TTkK -from TermTk.TTkAbstract.abstracttablemodel import TTkAbstractTableModel +from TermTk.TTkWidgets.TTkModelView.tablemodellist import TTkTableModelList -class TTkTableModelCSV(TTkAbstractTableModel): - __slots__ = ('_list', '_hheader', '_vheader') +class TTkTableModelCSV(TTkTableModelList): def __init__(self, *, filename='', fd=None): - self._list = [] - self._hheader = [] - self._vheader = [] + ml, head, idx = [[]], [], [] if filename: with open(filename, "r") as fd: - self._csvImport(fd) + ml, head, idx = self._csvImport(fd) elif fd: - self._csvImport(fd) - super().__init__() + ml, head, idx = self._csvImport(fd) + super().__init__(list=ml,header=head,indexes=idx) - def _csvImport(self, fd): + def _csvImport(self, fd) -> tuple[list,list,list[list]]: + ml, head, idx = [], [], [] sniffer = csv.Sniffer() has_header = sniffer.has_header(fd.read(2048)) fd.seek(0) csvreader = csv.reader(fd) for row in csvreader: - self._list.append(row) + ml.append(row) if has_header: - self._hheader = self._list.pop(0) + head = ml.pop(0) # check if the first column include an index: - if self._checkIndexColumn(): - self._hheader.pop(0) - for l in self._list: - self._vheader.append(l.pop(0)) - - def _checkIndexColumn(self): - if all(l[0].isdigit() for l in self._list): - num = int(self._list[0][0]) - return all(num+i==int(l[0]) for i,l in enumerate(self._list)) + if self._checkIndexColumn(ml): + head.pop(0) + for l in ml: + idx.append(l.pop(0)) + return ml, head, idx + + def _checkIndexColumn(self, ml:list[list]) -> bool: + if all(l[0].isdigit() for l in ml): + num = int(ml[0][0]) + return all(num+i==int(l[0]) for i,l in enumerate(ml)) return False - - def rowCount(self): - return len(self._list) - - def columnCount(self): - return len(self._list[0]) if self._list else 0 - - def data(self, row, col): - return self._list[row][col] - - def setData(self, row, col, data): - self._list[row][col] = data - - def headerData(self, num, orientation): - if orientation == TTkK.HORIZONTAL: - if self._hheader: - return self._hheader[num] - if orientation == TTkK.VERTICAL: - if self._vheader: - return self._vheader[num] - return super().headerData(num, orientation) \ No newline at end of file diff --git a/TermTk/TTkWidgets/TTkModelView/tablemodeljson.py b/TermTk/TTkWidgets/TTkModelView/tablemodeljson.py new file mode 100644 index 00000000..e69de29b diff --git a/TermTk/TTkWidgets/TTkModelView/tablemodellist.py b/TermTk/TTkWidgets/TTkModelView/tablemodellist.py new file mode 100644 index 00000000..ed4ff254 --- /dev/null +++ b/TermTk/TTkWidgets/TTkModelView/tablemodellist.py @@ -0,0 +1,71 @@ +# 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. + +__all__=['TTkTableModelList'] + +from TermTk.TTkCore.constant import TTkK +from TermTk.TTkAbstract.abstracttablemodel import TTkAbstractTableModel + +class TTkTableModelList(TTkAbstractTableModel): + __slots__ = ('_list','_listOriginal', '_hheader', '_vheader') + def __init__(self, *, list=[], header=[], indexes=[]): + self._list = self._listOriginal = list + self._hheader = header + self._vheader = indexes + super().__init__() + + def modelList(self) -> list[list]: + return self._list + + def setModelList(self, modelList:list[list]) -> None: + if modelList == self._list: return + self._list = modelList + + def rowCount(self) -> int: + return len(self._list) + + def columnCount(self) -> int: + return len(self._list[0]) if self._list else 0 + + def data(self, row:int, col:int) -> None: + return self._list[row][col] + + def setData(self, row:int, col:int, data:object) -> None: + self._list[row][col] = data + + def headerData(self, num:int, orientation:int): + if orientation == TTkK.HORIZONTAL: + if self._hheader: + return self._hheader[num] + if orientation == TTkK.VERTICAL: + if self._vheader: + return self._vheader[num] + return super().headerData(num, orientation) + + def sort(self, column:int, order:TTkK.SortOrder) -> None: + if column == -1: + self._list = self._listOriginal + else: + try: + self._list = sorted(self._listOriginal, key=lambda x:x[column], reverse=order==TTkK.SortOrder.DescendingOrder) + except TypeError as _: + self._list = sorted(self._listOriginal, key=lambda x:str(x[column]), reverse=order==TTkK.SortOrder.DescendingOrder) diff --git a/TermTk/TTkWidgets/TTkModelView/tablewidget.py b/TermTk/TTkWidgets/TTkModelView/tablewidget.py index 16d0b104..1d0c81a7 100644 --- a/TermTk/TTkWidgets/TTkModelView/tablewidget.py +++ b/TermTk/TTkWidgets/TTkModelView/tablewidget.py @@ -125,8 +125,11 @@ class TTkTableWidget(TTkAbstractScrollView): '_showVSeparators', '_showHSeparators', '_verticalHeader', '_horizontallHeader', '_colsPos', '_rowsPos', + '_sortingEnabled', + '_dataPadding', '_internal', - '_selected', '_hSeparatorSelected', '_vSeparatorSelected', + '_selected', '_selectedBase', + '_hSeparatorSelected', '_vSeparatorSelected', '_hoverPos', '_dragPos', '_currentPos', '_sortColumn', '_sortOrder', '_fastCheck', '_guessDataEdit', @@ -137,6 +140,7 @@ class TTkTableWidget(TTkAbstractScrollView): def __init__(self, *, tableModel:TTkAbstractTableModel=None, vSeparator:bool=True, hSeparator:bool=True, + sortingEnabled=False, dataPadding=1, **kwargs) -> None: # Signals # self.itemActivated = pyTTkSignal(TTkTableWidgetItem, int) @@ -148,11 +152,14 @@ class TTkTableWidget(TTkAbstractScrollView): self._fastCheck = True self._guessDataEdit = True + self._dataPadding = dataPadding + self._sortingEnabled = sortingEnabled self._showHSeparators = vSeparator self._showVSeparators = hSeparator self._verticalHeader = _HeaderView() self._horizontallHeader = _HeaderView() self._selected = None + self._selectedBase = None self._hoverPos = None self._dragPos = None self._currentPos = None @@ -172,6 +179,50 @@ class TTkTableWidget(TTkAbstractScrollView): self._verticalHeader.visibilityUpdated.connect( self._headerVisibilityChanged) self._horizontallHeader.visibilityUpdated.connect(self._headerVisibilityChanged) + + @pyTTkSlot(bool) + def setSortingEnabled(self, enable:bool) -> None: + ''' + If enable is true, enables sorting for the table and immediately trigger a call to sortByColumn() with the current sort section and order + Note: Setter function for property sortingEnabled. + + :param enable: the availability of undo + :type enable: bool + ''' + if enable == self._sortingEnabled: return + self._sortingEnabled = enable + self.sortByColumn(self._sortColumn, self._sortOrder) + + def isSortingEnabled(self) -> bool: + ''' + This property holds whether sorting is enabled + + If this property is true, sorting is enabled for the table. If this property is false, sorting is not enabled. The default value is false. + + Note: . Setting the property to true with setSortingEnabled() immediately triggers a call to sortByColumn() with the current sort section and order. + + This property was introduced in Qt 4.2. + ''' + return self._sortingEnabled + + @pyTTkSlot(int, TTkK.SortOrder) + def sortByColumn(self, column:int, order:TTkK.SortOrder) -> None: + ''' + Sorts the model by the values in the given column and order. + + column may be -1, in which case no sort indicator will be shown and the model will return to its natural, unsorted order. Note that not all models support this and may even crash in this case. + + :param column: the column used for the sorting + :type column: bool + + :param order: the sort order + :type order: :class:`~TermTk.TTkCore.constant.TTkK.SortOrder` + ''' + self._sortColumn = column + self._sortOrder = order + self._tableModel.sort(column,order) + self.update() + @pyTTkSlot() def _headerVisibilityChanged(self): showVH = self._verticalHeader.isVisible() @@ -195,6 +246,8 @@ class TTkTableWidget(TTkAbstractScrollView): self._rowsPos = [1+x*2 for x in range(rows)] else: self._rowsPos = [1+x for x in range(rows)] + # self._selectedBase = sb = [False]*cols + # self._selected = [sb]*rows self._selected = [[False]*cols for _ in range(rows)] # Overridden function @@ -214,8 +267,12 @@ class TTkTableWidget(TTkAbstractScrollView): def setSelection(self, pos:tuple[int,int], size:tuple[int,int], flags:TTkK.TTkItemSelectionModel): x,y = pos w,h = size - for line in self._selected[y:y+h]: - line[x:x+w]=[True]*w + if flags & (TTkK.TTkItemSelectionModel.Clear|TTkK.TTkItemSelectionModel.Deselect): + for line in self._selected[y:y+h]: + line[x:x+w]=[False]*w + elif flags & TTkK.TTkItemSelectionModel.Select: + for line in self._selected[y:y+h]: + line[x:x+w]=[True]*w self.update() @pyTTkSlot() @@ -224,6 +281,22 @@ class TTkTableWidget(TTkAbstractScrollView): self.layout().setOffset(-x,-y) self.update() + def rowCount(self) -> int: + return self._tableModel.rowCount() + + def currentRow(self) -> int: + if _cp := self._currentPos: + return _cp[0] + return 0 + + def columnCount(self) -> int: + return self._tableModel.columnCount() + + def currentColumn(self) -> int: + if _cp := self._currentPos: + return _cp[1] + return 0 + def verticalHeader(self): return self._verticalHeader @@ -261,9 +334,6 @@ class TTkTableWidget(TTkAbstractScrollView): self._refreshLayout() self.viewChanged.emit() - def setSortingEnabled(self, enable) -> None: - pass - def focusOutEvent(self) -> None: self._hSeparatorSelected = None self._vSeparatorSelected = None @@ -299,7 +369,7 @@ class TTkTableWidget(TTkAbstractScrollView): rowa,rowb = max(0,row-100), min(row+100,rows) else: rowa,rowb = 0,rows - return max(_wid(i) for i in range(rowa,rowb)) + return max(_wid(i) for i in range(rowa,rowb))+self._dataPadding @pyTTkSlot(int) def resizeColumnToContents(self, column:int) -> None: @@ -670,6 +740,13 @@ class TTkTableWidget(TTkAbstractScrollView): self._hSeparatorSelected = i self.update() return True + elif self._sortingEnabled and _x == c-1: # Pressed the sort otder icon + if self._sortColumn == i: + order = TTkK.SortOrder.DescendingOrder if self._sortOrder==TTkK.SortOrder.AscendingOrder else TTkK.SortOrder.AscendingOrder + else: + order = TTkK.SortOrder.AscendingOrder + self.sortByColumn(i,order) + return True elif _x < c: # # I-th header selected # order = not self._sortOrder if self._sortColumn == i else TTkK.AscendingOrder @@ -929,7 +1006,7 @@ class TTkTableWidget(TTkAbstractScrollView): if xa>w : break if xb +# +# 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. + +# Demo inspired from: +# https://www.daniweb.com/programming/software-development/code/447834/applying-pyside-s-qabstracttablemodel + +import os +import sys +import csv +import re +import argparse +import operator +import json +import random + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +parser = argparse.ArgumentParser() +parser.add_argument('-f', help='Full Screen', action='store_true') +parser.add_argument('-t', help='Track Mouse', action='store_true') +parser.add_argument('--csv', help='Open CSV File', type=argparse.FileType('r')) + +args = parser.parse_args() + +fullScreen = args.f +mouseTrack = args.t + +# csvData = [] +# if args.csv: +# sniffer = csv.Sniffer() +# has_header = sniffer.has_header(args.csv.read(2048)) +# args.csv.seek(0) +# csvreader = csv.reader(args.csv) +# for row in csvreader: +# csvData.append(row) + + +imagesFile = os.path.join(os.path.dirname(os.path.abspath(__file__)),'../ansi.images.json') +with open(imagesFile) as f: + d = json.load(f) + # Image exported by the Dumb Paint Tool - Removing the extra '\n' at the end + diamond = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['diamond' ])[0:-1]) + fire = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['fire' ])[0:-1]) + fireMini = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['fireMini'])[0:-1]) + key = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['key' ])[0:-1]) + peach = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['peach' ])[0:-1]) + pepper = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['pepper' ])[0:-1]) + python = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['python' ])[0:-1]) + ring = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['ring' ])[0:-1]) + sword = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['sword' ])[0:-1]) + whip = ttk.TTkString(ttk.TTkUtil.base64_deflate_2_obj(d['compressed']['whip' ])[0:-1]) + tiles = [diamond,fire,key,peach,ring,sword,whip] + images = [fireMini,pepper,python] + + +class CustomColorModifier(ttk.TTkAlternateColor): + colors = ( + [ ttk.TTkColor.bg("#000066"), ttk.TTkColor.bg("#0000FF") ] * 3 + + [ ttk.TTkColor.bg("#003300"), ttk.TTkColor.bg("#006600") ] + + [ ttk.TTkColor.bg("#000066"), ttk.TTkColor.bg("#0000FF") ] * 3 + + [ttk.TTkColor.RST] * 5 + + [ #Rainbow-ish + ttk.TTkColor.fgbg("#00FFFF","#880000") + ttk.TTkColor.BOLD, + ttk.TTkColor.fgbg("#00FFFF","#FF0000") + ttk.TTkColor.BOLD, + ttk.TTkColor.fgbg("#0000FF","#FFFF00") + ttk.TTkColor.BOLD, + ttk.TTkColor.fgbg("#FF00FF","#00FF00") + ttk.TTkColor.BOLD, + ttk.TTkColor.fgbg("#FF0000","#00FFFF") + ttk.TTkColor.BOLD, + ttk.TTkColor.fgbg("#FFFF00","#0000FF") + ttk.TTkColor.BOLD, + ttk.TTkColor.fgbg("#00FF00","#FF00FF") + ttk.TTkColor.BOLD, + ttk.TTkColor.fgbg("#00FF00","#880088") + ttk.TTkColor.BOLD] + + [ttk.TTkColor.RST] * 3 + + [ttk.TTkColor.bg("#0000FF")] + + [ttk.TTkColor.RST] * 2 + + [ttk.TTkColor.bg("#0000FF")] + + [ttk.TTkColor.RST] + + [ttk.TTkColor.bg("#0000FF")] + + [ttk.TTkColor.RST] + + [ttk.TTkColor.bg("#0000FF")] * 2 + + [ttk.TTkColor.RST] * 3 + + [ttk.TTkColor.bg("#0000FF")] * 3 + + [ttk.TTkColor.RST] * 5 + + #Rainbow-ish 2 + [ttk.TTkColor.fgbg("#00FF00","#880088")] * 2 + + [ttk.TTkColor.fgbg("#00FF00","#FF00FF")] * 2 + + [ttk.TTkColor.fgbg("#FFFF00","#0000FF")] * 2 + + [ttk.TTkColor.fgbg("#FF0000","#00FFFF")] * 2 + + [ttk.TTkColor.fgbg("#FF00FF","#00FF00")] * 2 + + [ttk.TTkColor.fgbg("#0000FF","#FFFF00")] * 2 + + [ttk.TTkColor.fgbg("#00FFFF","#FF0000")] * 2 + + [ttk.TTkColor.fgbg("#00FFFF","#880000")] * 2 + + [ttk.TTkColor.RST] * 2 + ) + def __init__(self): + super().__init__() + + def exec(self, x:int, y:int, base_color:ttk.TTkColor) -> ttk.TTkColor: + c = CustomColorModifier.colors + return c[y%len(c)] + + +class MyTableModel(ttk.TTkTableModelList): + def __init__(self, mylist, size=None): + self.size=size + super().__init__(list=mylist) + + def rowCount(self): return super().rowCount() if not self.size else self.size[0] + def columnCount(self): return super().columnCount() if not self.size else self.size[1] + + def headerData(self, num, orientation): + if orientation == ttk.TTkK.HORIZONTAL: + prefix = ['H-aa','H-bb','H-cc','H-dd','H-ee','H-ff','H-gg','Parodi','H-hh',] + return f"{prefix[num%len(prefix)]}:{num:03}" + if orientation == ttk.TTkK.VERTICAL: + prefix = ['aa','bb','cc','dd','ee','ff','gg','Euge'] + return f"{prefix[num%len(prefix)]}:{num:03}" + return super().headerData(num, orientation) + + +txt1 = "Text" +txt2 = txt1*5 +txt3 = 'M1: '+' -M1\n'.join([txt1*2]*3) +txt4 = 'M2: '+' -M2\n'.join([txt1*5]*5) +txt5 = ttk.TTkString(txt4, ttk.TTkColor.RED + ttk.TTkColor.BG_YELLOW) + +# use numbers for numeric data to sort properly +p1 = 4 +p2 = 2 +p3 = 2 +p4 = 2 + +data_list1 = [ + [f"{x:04}"]+ + [txt1,txt2,txt3,txt4,txt5]+ + [random.choice(tiles*p1+images*p2+[txt1,txt2]*p3+[123,234,345,567,890,123.001,234.02,345.3,567.04,890.01020304,1]*p4)]+ + [random.choice(tiles*p1+images*p2+[txt1,txt2]*p3+[123,234,345,567,890,123.001,234.02,345.3,567.04,890.01020304,1]*p4)]+ + [random.choice(tiles*p1+images*p2+[txt1,txt2]*p3+[123,234,345,567,890,123.001,234.02,345.3,567.04,890.01020304,1]*p4)]+ + [random.choice(tiles*p1+images*p2+[txt1,txt2]*p3+[123,234,345,567,890,123.001,234.02,345.3,567.04,890.01020304,1]*p4)]+ + [random.choice(tiles*p1+images*p2+[txt1,txt2]*p3+[123,234,345,567,890,123.001,234.02,345.3,567.04,890.01020304,1]*p4)]+ + [y for y in range(10)]+ + [y+0.123 for y in range(10)] + for x in range(5000)] + +data_list2 = [ + [txt1,txt2,txt3]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [y for y in range(10)]+ + [y+0.123 for y in range(10)] + for x in range(1000)] + +data_list3 = [ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)]+ + [random.choice(tiles*p1)] + for x in range(30)] + + +root = ttk.TTk(title="pyTermTk Table Demo", mouseTrack=mouseTrack) +if fullScreen: + rootTable = root + root.setLayout(ttk.TTkGridLayout()) +else: + rootTable = ttk.TTkWindow(parent=root,pos = (0,0), size=(150,40), title="Test Table 1", layout=ttk.TTkGridLayout(), border=True) + +splitter = ttk.TTkSplitter(parent=rootTable,orientation=ttk.TTkK.VERTICAL) + + + +table_model1 = MyTableModel(data_list1) +table_model2 = MyTableModel(data_list2) +table_model3 = MyTableModel(data_list3) +# table_model = MyTableModel(data_list, size=(15,10)) + +table = ttk.TTkTable(parent=splitter, tableModel=table_model1) + +# # set column width to fit contents (set font first!) +# table.resizeColumnsToContents() +# # enable sorting +# table.setSortingEnabled(True) + +table.setSelection((0,0),(2,2),ttk.TTkK.TTkItemSelectionModel.Select) +table.setSelection((3,0),(1,2),ttk.TTkK.TTkItemSelectionModel.Select) + +table.setSelection((1,3),(2,4),ttk.TTkK.TTkItemSelectionModel.Select) +table.setSelection((2,5),(2,4),ttk.TTkK.TTkItemSelectionModel.Select) +table.setSelection((0,9),(2,3),ttk.TTkK.TTkItemSelectionModel.Select) + +table.setSelection((1,59),(1,2),ttk.TTkK.TTkItemSelectionModel.Select) +table.setSelection((3,59),(1,2),ttk.TTkK.TTkItemSelectionModel.Select) + +controlAndLogsSplitter = ttk.TTkSplitter() + +splitter.addWidget(controlAndLogsSplitter,size=8,title="LOGS") + +controls = ttk.TTkContainer() + +defaultStyle = table.style()['default'] + +tableStyle1 = {'default': defaultStyle|{'color': ttk.TTkColor.RST} } +tableStyle2 = {'default': defaultStyle|{'color': ttk.TTkColor.bg("#000066", modifier=ttk.TTkAlternateColor(alternateColor=ttk.TTkColor.BG_BLUE))} } +tableStyle3 = {'default': defaultStyle|{ + 'color': ttk.TTkColor.bg("#006600", modifier=ttk.TTkAlternateColor(alternateColor=ttk.TTkColor.bg("#003300"))), + 'selectedColor': ttk.TTkColor.bg("#00AA00"), + 'hoverColor': ttk.TTkColor.bg("#00AAAA")} } +tableStyle4 = {'default': defaultStyle|{'color': ttk.TTkColor.bg("#660066", modifier=ttk.TTkAlternateColor(alternateColor=ttk.TTkColor.RST))} } +tableStyle5 = {'default': defaultStyle|{ + 'color': ttk.TTkColor.bg("#000000", modifier=CustomColorModifier()), + 'lineColor': ttk.TTkColor.fg("#FFFF00"), + 'headerColor': ttk.TTkColor.fg("#FFFF00")+ttk.TTkColor.bg("#660066"), + 'hoverColor': ttk.TTkColor.bg("#AAAAAA"), + 'selectedColor': ttk.TTkColor.bg("#FFAA66"), + 'separatorColor': ttk.TTkColor.fg("#330055")+ttk.TTkColor.bg("#660066")} } + +# Header Checkboxes +ttk.TTkLabel(parent=controls, pos=(1,0), text="Header:") +ht = ttk.TTkCheckbox(parent=controls, pos=( 1,1), size=(8,1), text=' Top ',checked=True) +hl = ttk.TTkCheckbox(parent=controls, pos=(10,1), size=(8,1), text=' Left',checked=True) + +ht.toggled.connect(table.horizontalHeader().setVisible) +hl.toggled.connect(table.verticalHeader().setVisible) + +# Lines/Separator Checkboxes +ttk.TTkLabel(parent=controls, pos=(1,2), text="Lines:") +vli = ttk.TTkCheckbox(parent=controls, pos=( 1,3), size=(5,1), text=' V',checked=True) +hli = ttk.TTkCheckbox(parent=controls, pos=(10,3), size=(5,1), text=' H',checked=True) + +vli.toggled.connect(table.setVSeparatorVisibility) +hli.toggled.connect(table.setHSeparatorVisibility) + + +# Themes Control +t1 = ttk.TTkRadioButton(parent=controls, pos=(20,1), size=(11,1), text=' Theme 1', radiogroup='Themes', checked=True) +t2 = ttk.TTkRadioButton(parent=controls, pos=(20,2), size=(11,1), text=' Theme 2', radiogroup='Themes') +t3 = ttk.TTkRadioButton(parent=controls, pos=(20,3), size=(11,1), text=' Theme 3', radiogroup='Themes') +t4 = ttk.TTkRadioButton(parent=controls, pos=(20,4), size=(11,1), text=' Theme 4', radiogroup='Themes') +t5 = ttk.TTkRadioButton(parent=controls, pos=(20,5), size=(11,1), text=' Theme 5', radiogroup='Themes') + +t1.clicked.connect(lambda : table.mergeStyle(tableStyle1)) +t2.clicked.connect(lambda : table.mergeStyle(tableStyle2)) +t3.clicked.connect(lambda : table.mergeStyle(tableStyle3)) +t4.clicked.connect(lambda : table.mergeStyle(tableStyle4)) +t5.clicked.connect(lambda : table.mergeStyle(tableStyle5)) + + +# Model Picker +m1 = ttk.TTkRadioButton(parent=controls, pos=(33,1), size=(11,1), text=' Model 1', radiogroup='Models', checked=True) +m2 = ttk.TTkRadioButton(parent=controls, pos=(33,2), size=(11,1), text=' Model 2', radiogroup='Models') +m3 = ttk.TTkRadioButton(parent=controls, pos=(33,3), size=(11,1), text=' Model 3', radiogroup='Models') + +m1.clicked.connect(lambda : table.setModel(table_model1)) +m2.clicked.connect(lambda : table.setModel(table_model2)) +m3.clicked.connect(lambda : table.setModel(table_model3)) + +if args.csv: + table_model_csv = ttk.TTkTableModelCSV(fd=args.csv) + m_csv = ttk.TTkRadioButton(parent=controls, pos=(33,4), size=(11,1), text=' CSV', radiogroup='Models') + m_csv.clicked.connect(lambda : table.setModel(table_model_csv)) + + + +# Resize Button +rvb = ttk.TTkButton(parent=controls, pos=(1,5), size=( 3,1), text="V", border=False) +rhb = ttk.TTkButton(parent=controls, pos=(4,5), size=( 3,1), text="H", border=False) +rb = ttk.TTkButton(parent=controls, pos=(7,5), size=(11,1), text="Resize", border=False) + +rhb.clicked.connect(table.resizeRowsToContents) +rvb.clicked.connect(table.resizeColumnsToContents) +rb.clicked.connect( table.resizeRowsToContents) +rb.clicked.connect( table.resizeColumnsToContents) + +controlAndLogsSplitter.addWidget(controls, size=50) +controlAndLogsSplitter.addWidget(ttk.TTkLogViewer()) + +cbs = ttk.TTkCheckbox(parent=controls, pos=( 33,5), size=(8,1), text='-Sort', checked=False) + +cbs.toggled.connect(table.setSortingEnabled) + +wkb = ttk.TTkButton(parent=controls, pos=(43,5), size=( 4,1), text="🤌", border=False) + + +@ttk.pyTTkSlot() +def _showWinKey(): + winKey = ttk.TTkWindow(title="KeyPress",layout=ttk.TTkGridLayout(), size=(80,7)) + winKey.layout().addWidget(ttk.TTkKeyPressView(maxHeight=3)) + ttk.TTkHelper.overlay(None, winKey, 10, 4, toolWindow=True) + +wkb.clicked.connect(_showWinKey) + +table.setFocus() + +root.mainloop() \ No newline at end of file diff --git a/tests/timeit/02.array.05.sort.py b/tests/timeit/02.array.05.sort.py new file mode 100755 index 00000000..ca16d4ba --- /dev/null +++ b/tests/timeit/02.array.05.sort.py @@ -0,0 +1,82 @@ +#!/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 os +import sys +import argparse +import operator +import json +import timeit +import random + +sys.path.append(os.path.join(sys.path[0],'../..')) + +l1 =[ random.randint(0x100,0x20000) for _ in range(10000) ] +l2 =[ random.randint(0x100,0x20000) for _ in range(10000) ] + ['b'] + +def _k1(x): return x +def _k2(x): return str(x) + +def test_ti_1_01(): sorted(l1) +def test_ti_1_02(): sorted(l1, key=lambda x: x) +def test_ti_1_03(): sorted(l1, key=lambda x:str(x)) +def test_ti_1_04(): sorted(l1, key=_k1) +def test_ti_1_05(): sorted(l1, key=_k2) + +def test_ti_1_06(): + try: + sorted(l1) + except TypeError as _: + sorted(l1, key=lambda x:str(x)) + +def test_ti_1_07(): + try: + sorted(l1) + except TypeError as _: + sorted(l1, key=_k2) + +def test_ti_2_01(): sorted(l2, key=lambda x:str(x)) +def test_ti_2_02(): sorted(l2, key=_k2) + +def test_ti_2_03(): + try: + sorted(l2) + except TypeError as _: + sorted(l2, key=lambda x:str(x)) + +def test_ti_2_04(): + try: + sorted(l2) + except TypeError as _: + sorted(l2, key=_k2) + +loop = 300 + +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)}")