diff --git a/TermTk/TTkAbstract/abstracttablemodel.py b/TermTk/TTkAbstract/abstracttablemodel.py index dfd0f82d..4ae7acd6 100644 --- a/TermTk/TTkAbstract/abstracttablemodel.py +++ b/TermTk/TTkAbstract/abstracttablemodel.py @@ -177,7 +177,7 @@ class TTkAbstractTableModel(): The base class implementation returns false. This function and :meth:`data` must be reimplemented for editable models. - :param row: the row position od the data + :param row: the row position of the data :type row: int :param col: the column position of the data :type col: int @@ -192,6 +192,11 @@ class TTkAbstractTableModel(): ''' Returns the :class:`~TermTk.TTkCore.string.TTkString` reprsents the ddata stored in the row/column. + :param row: the row position of the data + :type row: int + :param col: the column position of the data + :type col: int + :return: :class:`~TermTk.TTkCore.string.TTkString` ''' data = self.data(row,col) @@ -222,6 +227,25 @@ class TTkAbstractTableModel(): return TTkString(str(pos)) return TTkString() + def flags(self, row:int, col:int) -> TTkK.ItemFlag: + ''' + Returns the item flags for the given row,column. + + The base class implementation returns a combination of flags that + enables the item (:class:`~TermTk.TTkCore.constant.TTkConstant.ItemFlag.ItemIsEnabled`) + and allows it to be selected (:class:`~TermTk.TTkCore.constant.TTkConstant.ItemFlag.ItemIsSelectable`). + + :param row: the row position od the data + :type row: int + :param col: the column position of the data + :type col: int + + :return: :class:`~TermTk.TTkCore.constant.TTkConstant.ItemFlag` + ''' + return ( + TTkK.ItemFlag.ItemIsEnabled | + TTkK.ItemFlag.ItemIsSelectable ) + def sort(self, column:int, order:TTkK.SortOrder) -> None: ''' Sorts the model by column in the given order. diff --git a/TermTk/TTkCore/constant.py b/TermTk/TTkCore/constant.py index f7c401ed..bb6b5fd4 100644 --- a/TermTk/TTkCore/constant.py +++ b/TermTk/TTkCore/constant.py @@ -424,6 +424,30 @@ class TTkConstant: ClearAndSelect = Clear | Select '''A combination of Clear and Select, provided for convenience.''' + class ItemFlag(int): + ''':class:`ItemFlag` describes the properties of an item + + .. autosummary:: + NoItemFlags + ItemIsSelectable + ItemIsEditable + ItemIsEnabled + ''' + NoItemFlags = 0x0000 + '''It does not have any properties set.''' + ItemIsSelectable = 0x0001 + '''It can be selected.''' + ItemIsEditable = 0x0002 + '''It can be edited.''' + # ItemIsDragEnabled = 0x0004 + # '''It can be dragged.''' + # ItemIsDropEnabled = 0x0008 + # '''It can be used as a drop target.''' + # ItemIsUserCheckable = 0x0010 + # '''It can be checked or unchecked by the user.''' + ItemIsEnabled = 0x0020 + '''The user can interact with the item.''' + # LayoutItem Types class LayoutItemTypes(int): '''Types used internally in :mod:`~TermTk.TTkLayouts` diff --git a/TermTk/TTkWidgets/TTkModelView/tablemodellist.py b/TermTk/TTkWidgets/TTkModelView/tablemodellist.py index 2fb3def0..df800283 100644 --- a/TermTk/TTkWidgets/TTkModelView/tablemodellist.py +++ b/TermTk/TTkWidgets/TTkModelView/tablemodellist.py @@ -104,6 +104,12 @@ class TTkTableModelList(TTkAbstractTableModel): return self._vheader[num] return super().headerData(num, orientation) + def flags(self, row:int, col:int) -> TTkK.ItemFlag: + return ( + TTkK.ItemFlag.ItemIsEnabled | + TTkK.ItemFlag.ItemIsEditable | + TTkK.ItemFlag.ItemIsSelectable ) + def sort(self, column:int, order:TTkK.SortOrder) -> None: if column == -1: self._data = self._dataOriginal diff --git a/TermTk/TTkWidgets/TTkModelView/tablewidget.py b/TermTk/TTkWidgets/TTkModelView/tablewidget.py index 4a00133a..79feb587 100644 --- a/TermTk/TTkWidgets/TTkModelView/tablewidget.py +++ b/TermTk/TTkWidgets/TTkModelView/tablewidget.py @@ -262,9 +262,11 @@ class TTkTableWidget(TTkAbstractScrollView): def _restoreSnapshot(self, snapId:int,newData=True): rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() - self._selected = [[False]*cols for _ in range(rows)] + self.clearSelection() for i in self._snapshot[snapId][1:]: - self._selected[i.dataIndex.row()][i.dataIndex.col()]=True + row=i.dataIndex.row() + col=i.dataIndex.col() + self.setSelection(pos=(col,row),size=(1,1),flags=TTkK.TTkItemSelectionModel.Select) i.dataIndex.setData(i.newData if newData else i.oldData) cpsi:TTkModelIndex = self._snapshot[snapId][0] self._currentPos = (cpsi.row(),cpsi.col()) @@ -456,7 +458,7 @@ class TTkTableWidget(TTkAbstractScrollView): 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)] + self.clearSelection() # Overridden function def viewFullAreaSize(self) -> tuple[int, int]: @@ -472,6 +474,28 @@ class TTkTableWidget(TTkAbstractScrollView): def viewDisplayedSize(self) -> tuple[int, int]: return self.size() + def clearSelection(self) -> None: + ''' + Deselects all selected items. + The current index will not be changed. + ''' + rows = self._tableModel.rowCount() + cols = self._tableModel.columnCount() + self._selected = [[False]*cols for _ in range(rows)] + self.update() + + def selectAll(self) -> None: + ''' + Selects all items in the view. + This function will use the selection behavior set on the view when selecting. + ''' + rows = self._tableModel.rowCount() + cols = self._tableModel.columnCount() + flagFunc = self._tableModel.flags + cmp = TTkK.ItemFlag.ItemIsSelectable + self._selected = [[cmp==(cmp&flagFunc(_r,_c)) for _c in range(cols)] for _r in range(rows)] + self.update() + def setSelection(self, pos:tuple[int,int], size:tuple[int,int], flags:TTkK.TTkItemSelectionModel) -> None: ''' Selects the items within the given rect and in accordance with the specified selection flags. @@ -485,12 +509,14 @@ class TTkTableWidget(TTkAbstractScrollView): ''' x,y = pos w,h = size + flagFunc = self._tableModel.flags + cmp = TTkK.ItemFlag.ItemIsSelectable 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 + for _r, line in enumerate(self._selected[y:y+h],y): + line[x:x+w]=[cmp==(cmp&flagFunc(_r,_c)) for _c in range(x,x+w)] self.update() def selectRow(self, row:int) -> None: @@ -501,18 +527,44 @@ class TTkTableWidget(TTkAbstractScrollView): :type row: int ''' cols = self._tableModel.columnCount() - self._selected[row] = [True]*cols + cmp = TTkK.ItemFlag.ItemIsSelectable + flagFunc = self._tableModel.flags + self._selected[row] = [cmp==(cmp&flagFunc(row,col)) for col in range(cols)] self.update() - def selectColumn(self, column:int) -> None: + def selectColumn(self, col:int) -> None: ''' Selects the given column in the table view - :param column: the column to be selected + :param col: the column to be selected + :type col: int + ''' + cmp = TTkK.ItemFlag.ItemIsSelectable + flagFunc = self._tableModel.flags + for row,line in enumerate(self._selected): + line[col] = cmp==(cmp&flagFunc(row,col)) + self.update() + + def unselectRow(self, row:int) -> None: + ''' + Unselects the given row in the table view + + :param row: the row to be unselected + :type row: int + ''' + cols = self._tableModel.columnCount() + self._selected[row] = [False]*cols + self.update() + + def unselectColumn(self, column:int) -> None: + ''' + Unselects the given column in the table view + + :param column: the column to be unselected :type column: int ''' - for row in self._selected: - self._selected[column] = True + for line in self._selected: + line[column] = False self.update() @pyTTkSlot() @@ -944,8 +996,8 @@ class TTkTableWidget(TTkAbstractScrollView): # Mark only the current cell as aselected rows = self._tableModel.rowCount() cols = self._tableModel.columnCount() - self._selected = [[False]*cols for _ in range(rows)] - self._selected[row][col]=True + self.clearSelection() + self.setSelection(pos=(col,row),size=(1,1),flags=TTkK.TTkItemSelectionModel.Select) data = self._tableModel.data(row, col) if type(data) is str: @@ -1154,31 +1206,38 @@ class TTkTableWidget(TTkAbstractScrollView): row,col = self._findCell(x,y, headers=True) if not row==col==-1: self._dragPos = [(row,col),(row,col)] - rows = self._tableModel.rowCount() - cols = self._tableModel.columnCount() _ctrl = evt.mod==TTkK.ControlModifier if row==col==-1: # Corner Press # Select Everything - self._selected = [[True]*cols for _ in range(rows)] + self.selectAll() elif col==-1: # Row select - state = all(self._selected[row]) + flagFunc = self._tableModel.flags + cmp = TTkK.ItemFlag.ItemIsSelectable + state = all(_sel for i,_sel in enumerate(self._selected[row]) if flagFunc(row,i)&cmp) if not _ctrl: - self._selected = [[False]*cols for _ in range(rows)] - self._selected[row] = [not state]*cols + self.clearSelection() + if state: + self.unselectRow(row) + else: + self.selectRow(row) elif row==-1: # Col select - state = all(line[col] for line in self._selected) + flagFunc = self._tableModel.flags + cmp = TTkK.ItemFlag.ItemIsSelectable + state = all(_sel[col] for i,_sel in enumerate(self._selected) if flagFunc(i,col)&cmp) if not _ctrl: - self._selected = [[False]*cols for _ in range(rows)] - for line in self._selected: - line[col] = not state + self.clearSelection() + if state: + self.unselectColumn(col) + else: + self.selectColumn(col) else: # Cell Select self._currentPos = (row,col) - if _ctrl: self._selected[row][col] = not self._selected[row][col] - else: self._selected[row][col] = True + self.setSelection(pos = (col,row), size = (1,1), + flags = TTkK.TTkItemSelectionModel.Clear if (self._selected[row][col] and _ctrl) else TTkK.TTkItemSelectionModel.Select) self._hoverPos = None self.update() return True @@ -1246,7 +1305,7 @@ class TTkTableWidget(TTkAbstractScrollView): state = self._selected[max(0,rowa)][max(0,cola)] else: # Clear the selection if no ctrl has been pressed - self._selected = [[False]*cols for _ in range(rows)] + self.clearSelection() if rowa == -1: cola,colb=min(cola,colb),max(cola,colb) @@ -1258,8 +1317,8 @@ class TTkTableWidget(TTkAbstractScrollView): cola,colb=min(cola,colb),max(cola,colb) rowa,rowb=min(rowa,rowb),max(rowa,rowb) - for line in self._selected[rowa:rowb+1]: - line[cola:colb+1] = [state]*(colb-cola+1) + self.setSelection(pos = (cola,rowa), size = (colb-cola+1,rowb-rowa+1), + flags = TTkK.TTkItemSelectionModel.Select if state else TTkK.TTkItemSelectionModel.Clear) self._hoverPos = None self._dragPos = None diff --git a/tests/t.ui/test.ui.032.table.08.py b/tests/t.ui/test.ui.032.table.08.py index 637c07e2..d5248cde 100755 --- a/tests/t.ui/test.ui.032.table.08.py +++ b/tests/t.ui/test.ui.032.table.08.py @@ -137,6 +137,17 @@ class MyTableModel(ttk.TTkTableModelList): 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) + txt1 = "Text" txt2 = txt1*5 @@ -255,32 +266,34 @@ tableStyle5 = {'default': defaultStyle|{ 'selectedColor': ttk.TTkColor.bg("#FFAA66"), 'separatorColor': ttk.TTkColor.fg("#330055")+ttk.TTkColor.bg("#660066")} } -quitBtn = ttk.TTkButton(parent=controls, pos=(0,0), size=(3,6), text="Q\nU\nI\nT") +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=(4,0), text="Header:") -ht = ttk.TTkCheckbox(parent=controls, pos=( 4,1), size=(8,1), text=' Top ',checked=True) -hl = ttk.TTkCheckbox(parent=controls, pos=(13,1), size=(8,1), text=' Left',checked=True) +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=(4,2), text="Lines:") -vli = ttk.TTkCheckbox(parent=controls, pos=( 4,3), size=(5,1), text=' V',checked=True) -hli = ttk.TTkCheckbox(parent=controls, pos=(13,3), size=(5,1), text=' H',checked=True) +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=(23,1), size=(11,1), text=' Theme 1', radiogroup='Themes', checked=True) -t2 = ttk.TTkRadioButton(parent=controls, pos=(23,2), size=(11,1), text=' Theme 2', radiogroup='Themes') -t3 = ttk.TTkRadioButton(parent=controls, pos=(23,3), size=(11,1), text=' Theme 3', radiogroup='Themes') -t4 = ttk.TTkRadioButton(parent=controls, pos=(23,4), size=(11,1), text=' Theme 4', radiogroup='Themes') -t5 = ttk.TTkRadioButton(parent=controls, pos=(23,5), size=(11,1), text=' Theme 5', radiogroup='Themes') +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)) @@ -290,10 +303,10 @@ t5.clicked.connect(lambda : table.mergeStyle(tableStyle5)) # Model Picker -m1 = ttk.TTkRadioButton(parent=controls, pos=(36,0), size=(11,1), text=' Model 1', radiogroup='Models', checked=True) -m2 = ttk.TTkRadioButton(parent=controls, pos=(36,1), size=(11,1), text=' Model 2', radiogroup='Models') -m3 = ttk.TTkRadioButton(parent=controls, pos=(36,2), size=(11,1), text=' Model 3', radiogroup='Models') -m4 = ttk.TTkRadioButton(parent=controls, pos=(36,3), size=(11,1), text=' Model 4', radiogroup='Models') +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)) @@ -302,30 +315,30 @@ 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=(36,4), size=(11,1), text=' CSV', radiogroup='Models') + 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=( 4,5), size=( 3,1), text="C", border=False) -rrb = ttk.TTkButton(parent=controls, pos=( 7,5), size=( 3,1), text="R", border=False) -rb = ttk.TTkButton(parent=controls, pos=(10,5), size=(11,1), text="Resize", border=False) +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) -controlAndLogsSplitter.addWidget(controls, size=50) +controlAndLogsSplitter.addWidget(controls, size=51) controlAndLogsSplitter.addWidget(ttk.TTkLogViewer()) -cbs = ttk.TTkCheckbox(parent=controls, pos=( 36,5), size=(8,1), text='-Sort', checked=False) +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=(46,4), size=( 4,1), text="👌", border=False) -wkb = ttk.TTkButton(parent=controls, pos=(46,5), size=( 4,1), text="🤌", border=False) +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) @ttk.pyTTkSlot()