diff --git a/Lib/vanilla/test/list2/columns.py b/Lib/vanilla/test/list2/columns.py new file mode 100644 index 0000000..9fd1df4 --- /dev/null +++ b/Lib/vanilla/test/list2/columns.py @@ -0,0 +1,96 @@ +import pprint +import vanilla + + +def makeItems(): + items = [] + letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + for i, letter in enumerate(letters): + number = i + 1 + item = dict( + letter=letter, + number=number + ) + items.append(item) + return items + + +class Test: + + def __init__(self): + self.w = vanilla.Window((500, 300)) + self.w.line = vanilla.VerticalLine("auto") + + columnDescriptions = [ + dict( + identifier="letter", + title="Letter", + sortable=True + ), + dict( + identifier="number", + title="Number", + sortable=True, + ) + ] + self.w.list = vanilla.List2( + "auto", + items=makeItems(), + columnDescriptions=columnDescriptions, + ) + self.w.appendColumnButton = vanilla.Button( + "auto", + "Append Column", + callback=self.appendColumnButtonCallback + ) + self.w.removeColumnButton = vanilla.Button( + "auto", + "Remove Column", + callback=self.removeColumnButtonCallback + ) + self.w.getButton = vanilla.Button( + "auto", + "Get Values", + callback=self.getButtonCallback + ) + rules = [ + "H:|-[list]-|", + "H:[getButton]-[appendColumnButton]-[removeColumnButton]-|", + "V:|-[list]-[appendColumnButton]-|", + "V:|-[list]-[removeColumnButton]-|", + "V:|-[list]-[getButton]-|" + ] + self.w.addAutoPosSizeRules(rules) + self.w.open() + + def appendColumnButtonCallback(self, sender): + try: + self.w.list.insertColumn( + 1, + dict( + identifier="punctuation", + title="Punctuation", + sortable=True, + editable=True + ), + ) + except Exception as e: + print(e) + + def removeColumnButtonCallback(self, sender): + self.w.list.removeColumn( + "punctuation" + ) + + def getButtonCallback(self, sender): + for i, item in enumerate(self.w.list.get()): + print(f"{i} ----------") + for k, v in item.items(): + v = pprint.pformat(v) + print(f"{k} : {v}") + print("\n") + + +if __name__ == "__main__": + from vanilla.test.testTools import executeVanillaTest + executeVanillaTest(Test) diff --git a/Lib/vanilla/test/testAll.py b/Lib/vanilla/test/testAll.py index 482ea1d..ae447b5 100644 --- a/Lib/vanilla/test/testAll.py +++ b/Lib/vanilla/test/testAll.py @@ -308,7 +308,7 @@ def __init__(self, drawGrid=False): simpleList = List((0, 0, 0, 0), listOptions, enableTypingSensitivity=True) multiItems = [ - {"name": name, "path": os.path.basename(getattr(module, "__file__", "Unknown"))} + {"name": name, "path": os.path.basename(getattr(module, "__file__", None) or "Unknow")} for name, module in sys.modules.items() ] columnDescriptions = [ diff --git a/Lib/vanilla/vanillaList2.py b/Lib/vanilla/vanillaList2.py index 876e1da..47214b2 100644 --- a/Lib/vanilla/vanillaList2.py +++ b/Lib/vanilla/vanillaList2.py @@ -57,20 +57,40 @@ def setGroupCellClassWithKwargs(self, cls, kwargs): self._groupRowCellClassKwargs = kwargs @python_method - def setGetters(self, getters): - self._valueGetters = getters + def addGetter(self, identifier, getter): + self._valueGetters[identifier] = getter @python_method - def setSetters(self, setters): - self._valueSetters = setters + def removeGetter(self, identifier): + if identifier in self._valueGetters: + del self._valueGetters[identifier] @python_method - def setValueToCellConverters(self, converters): - self._valueToCellConverters = converters + def addSetter(self, identifier, setter): + self._valueSetters[identifier] = setter @python_method - def setCellToValueConverters(self, converters): - self._cellToValueConverters = converters + def removeSetter(self, identifier): + if identifier in self._valueGetters: + del self._valueSetters[identifier] + + @python_method + def addValueToCellConverters(self, identifier, converter): + self._valueToCellConverters[identifier] = converter + + @python_method + def removeValueToCellConverters(self, identifier): + if identifier in self._valueToCellConverters: + del self._valueToCellConverters[identifier] + + @python_method + def addCellToValueConverters(self, identifier, converter): + self._cellToValueConverters[identifier] = converter + + @python_method + def removeCellToValueConverters(self, identifier): + if identifier in self._cellToValueConverters: + del self._cellToValueConverters[identifier] @python_method def vanillaWrapper(self): @@ -157,7 +177,7 @@ def getItemValueForColumnAndRow(self, identifier, row): elif function is not None: value = function(item) else: - value = item[identifier] + value = item.get(identifier, "") if identifier in self._valueToCellConverters: value = self._valueToCellConverters[identifier](value) return value @@ -638,84 +658,125 @@ def _getDropView(self): return self._tableView def _buildColumns(self, columnDescriptions): - getters = {} - setters = {} - cellToValueConverters = {} - valueToCellConverters = {} - rowHeights = [] if osVersionCurrent >= osVersion10_16: - rowHeights.append(24) + self._tableView.setRowHeight_(24) else: - rowHeights.append(17) + self._tableView.setRowHeight_(17) for columnDescription in columnDescriptions: - identifier = columnDescription["identifier"] - title = columnDescription.get("title", "") - width = columnDescription.get("width") - minWidth = columnDescription.get("minWidth", width) - maxWidth = columnDescription.get("maxWidth", width) - sortable = columnDescription.get("sortable") - cellClass = columnDescription.get("cellClass", EditTextList2Cell) - cellKwargs = columnDescription.get("cellClassArguments", {}) - editable = columnDescription.get("editable", False) - property = columnDescription.get("property") - getMethod = columnDescription.get("getMethod") - setMethod = columnDescription.get("setMethod") - getFunction = columnDescription.get("getFunction") - setFunction = columnDescription.get("setFunction") - cellToValueConverter = columnDescription.get("cellToValueConverter") - valueToCellConverter = columnDescription.get("valueToCellConverter") - getters[identifier] = dict( + self.appendColumn(columnDescription) + + def appendColumn(self, columnDescription): + """ + Append a column discription. + The column identifier has to be unique for this table. + """ + self.insertColumn(-1, columnDescription) + + def removeColumn(self, identifier): + """ + Remove a column by the `identifier`. + This will fail silently when the column does not exists. + """ + column = self._tableView.tableColumnWithIdentifier_(identifier) + if column: + self._tableView.removeTableColumn_(column) + self._dataSourceAndDelegate.removeGetter(identifier) + self._dataSourceAndDelegate.removeSetter(identifier) + self._dataSourceAndDelegate.removeValueToCellConverters(identifier) + self._dataSourceAndDelegate.removeCellToValueConverters(identifier) + + def insertColumn(self, index, columnDescription): + """ + Insert a column discription and an index. + The column identifier has to be unique for this table. + """ + identifier = columnDescription["identifier"] + # dont allow columns with the same identifier + assert not self._tableView.tableColumnWithIdentifier_(identifier), f"Column with identifier '{identifier}' already exists." + title = columnDescription.get("title", "") + width = columnDescription.get("width") + minWidth = columnDescription.get("minWidth", width) + maxWidth = columnDescription.get("maxWidth", width) + sortable = columnDescription.get("sortable") + cellClass = columnDescription.get("cellClass", EditTextList2Cell) + cellKwargs = columnDescription.get("cellClassArguments", {}) + editable = columnDescription.get("editable", False) + property = columnDescription.get("property") + getMethod = columnDescription.get("getMethod") + setMethod = columnDescription.get("setMethod") + getFunction = columnDescription.get("getFunction") + setFunction = columnDescription.get("setFunction") + cellToValueConverter = columnDescription.get("cellToValueConverter") + valueToCellConverter = columnDescription.get("valueToCellConverter") + + self._dataSourceAndDelegate.addGetter( + identifier, + dict( property=property, method=getMethod, function=getFunction ) - setters[identifier] = dict( + ) + self._dataSourceAndDelegate.addSetter( + identifier, + dict( property=property, method=setMethod, function=setFunction ) - if cellToValueConverter is not None: - cellToValueConverters[identifier] = cellToValueConverter - if valueToCellConverter is not None: - valueToCellConverters[identifier] = valueToCellConverter - cellKwargs["editable"] = editable - if editable: - cellKwargs["callback"] = True - self._dataSourceAndDelegate.setCellClassWithKwargsForColumn( - cellClass, cellKwargs, identifier + ) + + if cellToValueConverter is not None: + self._dataSourceAndDelegate.addCellToValueConverters( + identifier, + cellToValueConverter + ) + if valueToCellConverter is not None: + self._dataSourceAndDelegate.addValueToCellConverters( + identifier, + valueToCellConverter ) - if width is not None: - if width == minWidth and width == maxWidth: - resizingMask = AppKit.NSTableColumnNoResizing - else: - resizingMask = AppKit.NSTableColumnUserResizingMask | AppKit.NSTableColumnAutoresizingMask + + cellKwargs["editable"] = editable + if editable: + cellKwargs["callback"] = True + self._dataSourceAndDelegate.setCellClassWithKwargsForColumn( + cellClass, cellKwargs, identifier + ) + if width is not None: + if width == minWidth and width == maxWidth: + resizingMask = AppKit.NSTableColumnNoResizing else: resizingMask = AppKit.NSTableColumnUserResizingMask | AppKit.NSTableColumnAutoresizingMask - column = AppKit.NSTableColumn.alloc().initWithIdentifier_(identifier) - column.setTitle_(title) - column.setResizingMask_(resizingMask) - if width is not None: - column.setWidth_(width) - column.setMinWidth_(minWidth) - column.setMaxWidth_(maxWidth) - if self._allowsSorting and sortable: - sortDescriptor = AppKit.NSSortDescriptor.sortDescriptorWithKey_ascending_selector_( - identifier, - True, - "compare:" - ) - column.setSortDescriptorPrototype_(sortDescriptor) - self._tableView.addTableColumn_(column) - # measure the cell to get the row height - cell = cellClass(**cellKwargs) - height = cell._nsObject.fittingSize().height - del cell - rowHeights.append(height) - self._dataSourceAndDelegate.setGetters(getters) - self._dataSourceAndDelegate.setSetters(setters) - self._dataSourceAndDelegate.setCellToValueConverters(cellToValueConverters) - self._dataSourceAndDelegate.setValueToCellConverters(valueToCellConverters) - self._tableView.setRowHeight_(max(rowHeights)) + else: + resizingMask = AppKit.NSTableColumnUserResizingMask | AppKit.NSTableColumnAutoresizingMask + column = AppKit.NSTableColumn.alloc().initWithIdentifier_(identifier) + column.setTitle_(title) + column.setResizingMask_(resizingMask) + if width is not None: + column.setWidth_(width) + column.setMinWidth_(minWidth) + column.setMaxWidth_(maxWidth) + if self._allowsSorting and sortable: + sortDescriptor = AppKit.NSSortDescriptor.sortDescriptorWithKey_ascending_selector_( + identifier, + True, + "compare:" + ) + column.setSortDescriptorPrototype_(sortDescriptor) + self._tableView.addTableColumn_(column) + + if index != -1 and index != len(self._tableView.tableColumns()): + self._tableView.moveColumn_toColumn_( + self._tableView.columnWithIdentifier_(identifier), + index + ) + # measure the cell to get the row height + cell = cellClass(**cellKwargs) + height = cell._nsObject.fittingSize().height + del cell + if height > self._tableView.rowHeight(): + self._tableView.setRowHeight_(height) def _wrapItem(self, item): if isinstance(item, simpleDataTypes):