diff --git a/libs/pyTermTk/TermTk/TTkAbstract/abstracttablemodel.py b/libs/pyTermTk/TermTk/TTkAbstract/abstracttablemodel.py index 7e242969..b0e698a6 100644 --- a/libs/pyTermTk/TermTk/TTkAbstract/abstracttablemodel.py +++ b/libs/pyTermTk/TermTk/TTkAbstract/abstracttablemodel.py @@ -22,6 +22,8 @@ __all__ = ['TTkAbstractTableModel','TTkModelIndex'] +from typing import Tuple,Any + from TermTk.TTkCore.constant import TTkK from TermTk.TTkCore.string import TTkString from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot @@ -215,9 +217,19 @@ class TTkAbstractTableModel(): ''' return False + @staticmethod + def _dataToTTkString(data:Any) -> TTkString: + """Convert arbitrary data to TTkString (passes through TTkString, wraps str, else str() coercion).""" + if isinstance(data,TTkString): + return data + elif isinstance(data,str): + return TTkString(data) + else: + return TTkString(str(data)) + def ttkStringData(self, row:int, col:int) -> TTkString: ''' - Returns the :py:class:`TTkString` reprsents the ddata stored in the row/column. + Returns the :py:class:`TTkString` reprsents the data stored in the row/column. :param row: the row position of the data :type row: int @@ -227,12 +239,58 @@ class TTkAbstractTableModel(): :return: :py:class:`TTkString` ''' data = self.data(row,col) - if isinstance(data,TTkString): - return data - elif type(data) == str: - return TTkString(data) - else: - return TTkString(str(data)) + return TTkAbstractTableModel._dataToTTkString(data) + + def displayData(self, row:int, col:int) -> Tuple[TTkString, TTkK.Alignment]: + ''' + Returns the display representation and alignment for the data stored at the specified row/column position. + + This method provides the formatted text representation of the cell data along with its preferred alignment + for display purposes in table widgets. It combines the functionality of :meth:`ttkStringData` for text + formatting with alignment information for proper cell rendering. + + The default implementation returns the :py:class:`TTkString` representation of the data with left alignment. + Subclasses can override this method to provide custom formatting and alignment based on data type, + column purpose, or other display requirements. + + **Usage Examples:** + + - Numeric columns might return right alignment for better visual presentation + - Boolean columns might return center alignment with custom symbols + - Date columns might return formatted date strings with specific alignment + + :param row: The row position of the data (0-based index) + :type row: int + :param col: The column position of the data (0-based index) + :type col: int + + :return: A tuple containing the formatted text and its preferred alignment + :rtype: Tuple[:py:class:`TTkString`, :py:class:`TTkK.Alignment`] + + **Example Implementation:** + + .. code-block:: python + + def displayData(self, row: int, col: int) -> Tuple[TTkString, TTkK.Alignment]: + data = self.data(row, col) + + # Custom formatting based on column type + if isinstance(data, (int, float)): + # Right-align numeric data + return TTkString(f"{data:,.2f}"), TTkK.Alignment.RIGHT_ALIGN + elif isinstance(data, bool): + # Center-align boolean with custom symbols + symbol = "✓" if data else "✗" + return TTkString(symbol), TTkK.Alignment.CENTER_ALIGN + else: + # Default left-align for text + return self.ttkStringData(row, col), TTkK.Alignment.LEFT_ALIGN + ''' + data = self.data(row,col) + retData = TTkAbstractTableModel._dataToTTkString(data) + if isinstance(data, (int,float)): + return retData, TTkK.Alignment.RIGHT_ALIGN + return retData, TTkK.Alignment.LEFT_ALIGN def headerData(self, pos:int, orientation:TTkK.Direction) -> TTkString: ''' diff --git a/libs/pyTermTk/TermTk/TTkCore/canvas.py b/libs/pyTermTk/TermTk/TTkCore/canvas.py index fb8bcda3..e72e7682 100644 --- a/libs/pyTermTk/TermTk/TTkCore/canvas.py +++ b/libs/pyTermTk/TermTk/TTkCore/canvas.py @@ -212,7 +212,7 @@ class TTkCanvas(): self._set(y, x, char, color) - def drawTTkString(self, pos, text, width=None, color=TTkColor.RST, alignment=TTkK.NONE, forceColor=False): + def drawTTkString(self, pos, text:TTkString, width=None, color=TTkColor.RST, alignment=TTkK.NONE, forceColor=False): ''' NOTE: drawText is one of the most abused functions, diff --git a/libs/pyTermTk/TermTk/TTkCore/string.py b/libs/pyTermTk/TermTk/TTkCore/string.py index eabbd0c6..8636bb48 100644 --- a/libs/pyTermTk/TermTk/TTkCore/string.py +++ b/libs/pyTermTk/TermTk/TTkCore/string.py @@ -355,9 +355,13 @@ class TTkString(): ret._text = " " *p1 + self._text + " " *p2 ret._colors = [color]*p1 + self._colors + [color]*p2 elif alignment == TTkK.JUSTIFY: - # TODO: Text Justification - ret._text = self._text + " " *pad - ret._colors = self._colors + [color]*pad + if not (_slices := [_t for _t in self.split(" ") if _t._text]) or len(_slices) < 2: + return self + _avg_spaces = width - sum(_slice.termWidth() for _slice in _slices) + _avg_space = _avg_spaces // (len(_slices)-1) + _left_part = TTkString(' '*_avg_space).join(_slices[:-1]) + _last_space = width - _left_part.termWidth() - _slices[-1].termWidth() + return _left_part + TTkString(' '*_last_space) + _slices[-1] elif self._hasSpecialWidth is not None: # Trim the string to a fixed size taking care of the variable width unicode chars rt = "" diff --git a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py index 2df80a88..5111db24 100644 --- a/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py +++ b/libs/pyTermTk/TermTk/TTkWidgets/TTkModelView/tablewidget.py @@ -1587,14 +1587,16 @@ class TTkTableWidget(TTkAbstractScrollView): _cellsCache.append([row,col,xa,xb,ya,yb,cellColor]) def _drawCellContent(_col,_row,_xa,_xb,_ya,_yb,_color): - txt = self._tableModel.ttkStringData(_row, _col) + _txt, _align = self._tableModel.displayData(_row, _col) if _color != TTkColor.RST: - txt = txt.completeColor(_color) - for i,line in enumerate(txt.split('\n')): - y = i+_ya - canvas.drawTTkString(pos=(_xa,y), text=line, width=_xb-_xa, color=_color) - if y >= _yb-1: break - canvas.fill(pos=(_xa,y+1),size=(_xb-_xa,_yb-y-1),color=_color) + _txt = _txt.completeColor(_color) + for _i,_line in enumerate(_txt.split('\n')): + _y = _i+_ya + _width=_xb-_xa + _line = _line.align(width=_width, color=_color, alignment=_align) + canvas.drawTTkString(pos=(_xa,_y), text=_line, width=_width, color=_color) + if _y >= _yb-1: break + canvas.fill(pos=(_xa,_y+1),size=(_xb-_xa,_yb-_y-1),color=_color) def _drawCellBottom(_col,_row,_xa,_xb,_ya,_yb,cellColor): if _yb>=h: return diff --git a/tests/t.ui/test.ui.032.table.11.pyside.align.01.py b/tests/t.ui/test.ui.032.table.11.pyside.align.01.py new file mode 100755 index 00000000..01a773b8 --- /dev/null +++ b/tests/t.ui/test.ui.032.table.11.pyside.align.01.py @@ -0,0 +1,72 @@ +#!/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. + +from PySide6.QtWidgets import QApplication, QTableView +from PySide6.QtCore import Qt, QAbstractTableModel + +class MyTableModel(QAbstractTableModel): + def __init__(self, data): + super().__init__() + self._data = data + + def rowCount(self, parent=None): + return len(self._data) + + def columnCount(self, parent=None): + return len(self._data[0]) + + def data(self, index, role=Qt.DisplayRole): + if not index.isValid(): + return None + + value = self._data[index.row()][index.column()] + + if role == Qt.DisplayRole: + return value + + + if role == Qt.TextAlignmentRole: + if index.column() == 0: + return Qt.AlignLeft | Qt.AlignTop + elif index.column() == 1: + return Qt.AlignCenter + elif index.column() == 2: + return Qt.AlignRight | Qt.AlignVCenter + + return None + +app = QApplication([]) + +data = [ + ["Left", "Center\npippo", "Right"], + ["Apple", "Banana\npippo", "Cherry"], + ["Dog", "Elephant\npippo", "Fox"] +] + +model = MyTableModel(data) +view = QTableView() +view.setModel(model) +view.show() + +app.exec() \ No newline at end of file diff --git a/tests/t.ui/test.ui.032.table.13.alignment.01.py b/tests/t.ui/test.ui.032.table.13.alignment.01.py new file mode 100755 index 00000000..93afedaf --- /dev/null +++ b/tests/t.ui/test.ui.032.table.13.alignment.01.py @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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 +from typing import Tuple + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +parser = argparse.ArgumentParser() +parser.add_argument('-f', help='Full Screen (default)', action='store_true') +parser.add_argument('-w', help='Windowed', 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 = not args.w +mouseTrack = True + +# 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__(data=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) + + def flags(self, row: int, col: int) -> ttk.TTkConstant.ItemFlag: + if col==0: + return ( + ttk.TTkK.ItemFlag.ItemIsEnabled | + ttk.TTkK.ItemFlag.ItemIsSelectable ) + if col==1: + return ( + ttk.TTkK.ItemFlag.ItemIsEnabled | + ttk.TTkK.ItemFlag.ItemIsEditable ) + return super().flags(row, col) + + def displayData(self, row:int, col:int) -> Tuple[ttk.TTkString, ttk.TTkK.Alignment]: + if 0 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.LEFT_ALIGN + if 1 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.CENTER_ALIGN + if 2 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.RIGHT_ALIGN + if 3 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.JUSTIFY + return super().displayData(row,col) + +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"{y:03}\npippo\npeppo-ooo"]+[str(x) for x in range(10) ] for y in range(20)] +data_list1[3][3] = "abc\ndef\nghi\njkl" + +data_list2 = [ + [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_list3 = [ + [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_list4 = [ + [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, + sigmask=( + ttk.TTkTerm.Sigmask.CTRL_Q | + ttk.TTkTerm.Sigmask.CTRL_S | + ttk.TTkTerm.Sigmask.CTRL_C | + ttk.TTkTerm.Sigmask.CTRL_Z )) + +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_model4 = MyTableModel(data_list4) +# table_model = MyTableModel(data_list, size=(15,10)) + +table = ttk.TTkTable(parent=splitter, tableModel=table_model1) +# table = ttk.TTkTable(parent=splitter) + +# # 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=7,title="LOGS") + +controls = ttk.TTkContainer() +debugView = 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")} } + + +######################### +# Define the Controls # +######################### + +quitBtn = ttk.TTkButton(parent=controls, pos=(0,0), size=(5,6), text="Q\nU\nI\nT") +quitBtn.clicked.connect(root.quit) + +offsetQuit = 6 + +# Header Checkboxes +ttk.TTkLabel(parent=controls, pos=(offsetQuit,0), text="Header:") +ht = ttk.TTkCheckbox(parent=controls, pos=( offsetQuit ,1), size=(8,1), text=' Top ',checked=True) +hl = ttk.TTkCheckbox(parent=controls, pos=( offsetQuit+9,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=(offsetQuit,2), text="Lines:") +vli = ttk.TTkCheckbox(parent=controls, pos=(offsetQuit ,3), size=(5,1), text=' V',checked=True) +hli = ttk.TTkCheckbox(parent=controls, pos=(offsetQuit+9,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=(offsetQuit+19,1), size=(11,1), text=' Theme 1', radiogroup='Themes', checked=True) +t2 = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+19,2), size=(11,1), text=' Theme 2', radiogroup='Themes') +t3 = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+19,3), size=(11,1), text=' Theme 3', radiogroup='Themes') +t4 = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+19,4), size=(11,1), text=' Theme 4', radiogroup='Themes') +t5 = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+19,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=(offsetQuit+32,0), size=(11,1), text=' Model 1', radiogroup='Models', checked=True) +m2 = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+32,1), size=(11,1), text=' Model 2', radiogroup='Models') +m3 = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+32,2), size=(11,1), text=' Model 3', radiogroup='Models') +m4 = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+32,3), size=(11,1), text=' Model 4', 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)) +m4.clicked.connect(lambda : table.setModel(table_model4)) + +if args.csv: + table_model_csv = ttk.TTkTableModelCSV(fd=args.csv) + m_csv = ttk.TTkRadioButton(parent=controls, pos=(offsetQuit+32,4), size=(11,1), text=' CSV', radiogroup='Models') + m_csv.clicked.connect(lambda : table.setModel(table_model_csv)) + + + +# Resize Button +rcb = ttk.TTkButton(parent=controls, pos=(offsetQuit ,5), size=( 3,1), text="C", border=False) +rrb = ttk.TTkButton(parent=controls, pos=(offsetQuit+3,5), size=( 3,1), text="R", border=False) +rb = ttk.TTkButton(parent=controls, pos=(offsetQuit+6,5), size=(11,1), text="Resize", border=False) + +rrb.clicked.connect(table.resizeRowsToContents) +rcb.clicked.connect(table.resizeColumnsToContents) +rb.clicked.connect( table.resizeRowsToContents) +rb.clicked.connect( table.resizeColumnsToContents) + +cbs = ttk.TTkCheckbox(parent=controls, pos=(offsetQuit+32,5), size=(8,1), text='-Sort', checked=False) + +cbs.toggled.connect(table.setSortingEnabled) + +wtb = ttk.TTkButton(parent=controls, pos=(offsetQuit+41,4), size=( 4,1), text="👌", border=False) +wkb = ttk.TTkButton(parent=controls, pos=(offsetQuit+41,5), size=( 4,1), text="🤌", border=False) + +btn_ins_row = ttk.TTkButton(parent=controls, pos=(offsetQuit+45,0), size=( 4,1), text="❎", border=False) +btn_del_row = ttk.TTkButton(parent=controls, pos=(offsetQuit+45,1), size=( 4,1), text="❌", border=False) +btn_ins_col = ttk.TTkButton(parent=controls, pos=(offsetQuit+45,3), size=( 4,1), text="❎", border=False) +btn_del_col = ttk.TTkButton(parent=controls, pos=(offsetQuit+45,4), size=( 4,1), text="❌", border=False) + +######################### +# Define the DebugView # +######################### + +debugViewLayout = ttk.TTkGridLayout() +debugView.setLayout(debugViewLayout) + +debugViewLayout.addWidget(l_ch:=ttk.TTkLabel(text="Cell Changed: xxxx"),0,0) +debugViewLayout.addWidget(l_cl:=ttk.TTkLabel(text="Cell Clicked: xxxx"),1,0) +debugViewLayout.addWidget(l_dc:=ttk.TTkLabel(text="DoubleClked : xxxx"),2,0) +debugViewLayout.addWidget(l_ce:=ttk.TTkLabel(text="Cell Entered: xxxx"),3,0) +debugViewLayout.addWidget(l_cc:=ttk.TTkLabel(text="Changed: xxxx"),4,0) +debugViewLayout.addWidget(t_ed:=ttk.TTkTextEdit(lineNumber=True),0,1,6,1) + +table.cellChanged.connect( lambda r,c: l_ch.setText(f"Cell Changed: {r,c}")) +table.cellClicked.connect( lambda r,c: l_cl.setText(f"Cell Clicked: {r,c}")) +table.cellEntered.connect( lambda r,c: l_ce.setText(f"Cell Entered: {r,c}")) +table.cellDoubleClicked.connect( lambda r,c: l_dc.setText(f"DoubleClked : {r,c}")) +table.currentCellChanged.connect(lambda cr,cc,pr,pc: l_cc.setText(f"Changed:{cr,cc}<-{pr,pc}")) + +table.cellEntered.connect( lambda r,c: t_ed.setText(table.model().ttkStringData(r,c))) + +controlAndLogsSplitter.addWidget(controls, size=55) +controlAndLogsSplitter.addWidget(debugView, size=60) +controlAndLogsSplitter.addWidget(ttk.TTkLogViewer()) + +@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) + +@ttk.pyTTkSlot() +def _showWinText(): + winText = ttk.TTkWindow(title="Notepad",layout=ttk.TTkGridLayout(), size=(80,7)) + winText.layout().addWidget(ttk.TTkTextEdit(lineNumber=True, readOnly=False)) + ttk.TTkHelper.overlay(None, winText, 50, 20, toolWindow=True) + +@ttk.pyTTkSlot() +def _insertRows(): + _model = table.model() + _model.insertRows(3,2) + +@ttk.pyTTkSlot() +def _deleteRows(): + _model = table.model() + _model.removeRows(5,5) + +@ttk.pyTTkSlot() +def _insertCols(): + _model = table.model() + _model.insertColumns(3,2) + +@ttk.pyTTkSlot() +def _deleteCols(): + _model = table.model() + _model.removeColumns(5,5) + +btn_ins_row.clicked.connect(_insertRows) +btn_del_row.clicked.connect(_deleteRows) +btn_ins_col.clicked.connect(_insertCols) +btn_del_col.clicked.connect(_deleteCols) + +wtb.clicked.connect(_showWinText) + +table.setFocus() + +root.mainloop() \ No newline at end of file diff --git a/tests/t.ui/test.ui.032.table.13.alignment.02.overloading.py b/tests/t.ui/test.ui.032.table.13.alignment.02.overloading.py new file mode 100755 index 00000000..44cc0ebf --- /dev/null +++ b/tests/t.ui/test.ui.032.table.13.alignment.02.overloading.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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 +from typing import Tuple + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +parser = argparse.ArgumentParser() +parser.add_argument('-f', help='Full Screen (default)', action='store_true') +parser.add_argument('-w', help='Windowed', action='store_true') + +args = parser.parse_args() + +fullScreen = not args.w +mouseTrack = True + +class MyTableMixin(): + def headerData(self, num, orientation): + if orientation == ttk.TTkK.HORIZONTAL: + if 0 == num%4: + return f"{num} Left" + if 1 == num%4: + return f"{num} Center" + if 2 == num%4: + return f"{num} Right" + if 3 == num%4: + return f"{num} Justified" + return super().headerData(num, orientation) + + def displayData(self:ttk.TTkTableModelList, row:int, col:int) -> Tuple[ttk.TTkString, ttk.TTkK.Alignment]: + data, legacy_align = super().displayData(row,col) + if 0 == col%4: + return data, ttk.TTkK.Alignment.LEFT_ALIGN + if 1 == col%4: + return data, ttk.TTkK.Alignment.CENTER_ALIGN + if 2 == col%4: + return data, ttk.TTkK.Alignment.RIGHT_ALIGN + if 3 == col%4: + return data, ttk.TTkK.Alignment.JUSTIFY + return data, legacy_align + +class MyTableModel(MyTableMixin, ttk.TTkTableModelList): + def flags(self, row: int, col: int) -> ttk.TTkConstant.ItemFlag: + if col==0: + return ( + ttk.TTkK.ItemFlag.ItemIsEnabled | + ttk.TTkK.ItemFlag.ItemIsSelectable ) + if col==1: + return ( + ttk.TTkK.ItemFlag.ItemIsEnabled | + ttk.TTkK.ItemFlag.ItemIsEditable ) + return super().flags(row, col) + +data_list1 = [[f"{y:03}\npippo\npeppo-ooo"]+[str(x) for x in range(10) ] for y in range(20)] +data_list1[1][1] = "abc def ghi\ndef ghi\nghi\njkl - pippo" +data_list1[2][2] = "abc def ghi\ndef ghi\nghi\njkl - pippo" +data_list1[3][3] = "abc def ghi\ndef ghi a b c de fgh s\nghi\njkl - pippo" +data_list1[4][4] = "abc def ghi\ndef ghi\nghi\njkl - pippo" +data_list1[5][5] = "abc def ghi\ndef ghi\nghi\njkl - pippo" + +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) + +table_model1 = MyTableModel(data=data_list1) +table = ttk.TTkTable(parent=rootTable, tableModel=table_model1) +table.resizeRowsToContents() +table.resizeColumnsToContents() + +root.mainloop() \ No newline at end of file diff --git a/tests/t.ui/test.ui.032.table.13.alignment.03.mixin.py b/tests/t.ui/test.ui.032.table.13.alignment.03.mixin.py new file mode 100755 index 00000000..f6d65504 --- /dev/null +++ b/tests/t.ui/test.ui.032.table.13.alignment.03.mixin.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2025 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. + +# 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 +from typing import Tuple + +sys.path.append(os.path.join(sys.path[0],'../..')) +import TermTk as ttk + +parser = argparse.ArgumentParser() +parser.add_argument('-f', help='Full Screen (default)', action='store_true') +parser.add_argument('-w', help='Windowed', action='store_true') + +args = parser.parse_args() + +fullScreen = not args.w +mouseTrack = True + +class MyTableModel(ttk.TTkTableModelList): + def headerData(self, num, orientation): + if orientation == ttk.TTkK.HORIZONTAL: + if 0 == num%4: + return f"{num} Left" + if 1 == num%4: + return f"{num} Center" + if 2 == num%4: + return f"{num} Right" + if 3 == num%4: + return f"{num} Justified" + return super().headerData(num, orientation) + + def displayData(self:ttk.TTkTableModelList, row:int, col:int) -> Tuple[ttk.TTkString, ttk.TTkK.Alignment]: + if 0 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.LEFT_ALIGN + if 1 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.CENTER_ALIGN + if 2 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.RIGHT_ALIGN + if 3 == col%4: + return self.ttkStringData(row, col), ttk.TTkK.Alignment.JUSTIFY + return super().displayData(row,col) + +data_list1 = [[f"{y:03}\npippo\npeppo-ooo"]+[str(x) for x in range(10) ] for y in range(20)] +data_list1[1][1] = "abc def ghi\ndef ghi\nghi\njkl - pippo" +data_list1[2][2] = "abc def ghi\ndef ghi\nghi\njkl - pippo" +data_list1[3][3] = "abc def ghi\ndef ghi a b c de fgh s\nghi\njkl - pippo" +data_list1[4][4] = "abc def ghi\ndef ghi\nghi\njkl - pippo" +data_list1[5][5] = "abc def ghi\ndef ghi\nghi\njkl - pippo" + +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) + +table_model1 = MyTableModel(data=data_list1) +table = ttk.TTkTable(parent=rootTable, tableModel=table_model1) +table.resizeRowsToContents() +table.resizeColumnsToContents() + +root.mainloop() \ No newline at end of file