sclass JObjectTable is Swingable { new L data; transient IF1 itemToMap; transient Set hideFields; transient bool debug, fieldsInOrder = true, withSearcher; settable IVF1 defaultAction; transient gettable JTable table; transient TableSearcher searcher; transient bool useStruct = false; // possible breaking change for older modules event listChanged; *() { itemToMap = a -> { if (a instanceof S) ret litorderedmap("" := (S) a); Map map = humanizeKeys(fieldsInOrder ? objectToMap_inOrder_withoutFields(a, hideFields) : objectToMap(a)); if (!useStruct) map = mapValues strOrEmpty(map); ret map; }; } void onListChangedAndNow(Runnable r) { if (r == null) ret; onListChanged(r); r.run(); } cachedVisualize { L l = map(itemToMap, data); table = dataToTable_uneditable(sexyTable(), l); onDoubleClickOrEnter(table, voidfunc(int row) { A a = _get(data, row); if (a != null) defaultAction?.get(a); }); if (withSearcher) ret (searcher = tableWithSearcher2(table)).panel; ret jscroll(table); } void updateTable() { if (table != null) swing { Point scrollPosition = enclosingViewPosition(table); if (debug) print("Scroll position: " + scrollPosition); dataToTable_uneditable(table, map(itemToMap, data)); setEnclosingViewPosition(table, scrollPosition); if (searcher != null) searcher.rowIndices = null; if (debug) print("dataToTable done, alerting " + n2(onListChanged, "listener")); } listChanged(); } void clear() { syncClear(data); fireDataChanged(); } void add(A a) { syncAdd(data, a); fireDataChanged(); } A addAndReturn(A a) { add(a); ret a; } void add(int idx, A a) { syncAdd(data, idx, a); fireDataChanged(); } void addAll(Collection l) { if (empty(l)) ret; syncAddAll(data, l); fireDataChanged(); } void addAndScrollDown(A a) { add(a); scrollDown(); } void remove(A a) { syncRemove(data, a); fireDataChanged(); } void removeAll(L a) { syncRemoveAll(data, a); fireDataChanged(); } void setList(Iterable data) { setData(data); } void removeSelected() { removeAll(allSelected()); } void setData(Iterable data, bool force default false) { swing { int[] selection = selectedTableRows_array(table); L cloned = cloneList(data); if (force || !eq(this.data, cloned)) { this.data = cloned; updateTable(); } selectTableRows(table, selection); } } int count() { ret syncL(data); } void setData_force(Iterable data) { setData(data, true); } // tries to keep selection void fireDataChanged() { setData_force(data); } int rowFromSearcher(int i) { ret searcher == null || searcher.rowIndices == null ? i : or(get(searcher.rowIndices, i), -1); } A selected() { ret syncGet(data, rowFromSearcher(selectedTableRowInModel(table))); } L allSelected() { ret syncListGetMulti(data, selectedIndices()); } int selectedIndex() { ret selectedTableRow(table); } L selectedIndices() { ret map(i -> rowFromSearcher(i), selectedTableRowsInModel(table)); } bool selectItem(A a) { int i = indexOf(data, a); selectRow(table, i); ret i >= 0; } // true if item exists void doubleClickItem(A a) { if (selectItem(a)) defaultAction?.get(a); } // run only after visualized! void popupMenuItem(S text, IVF1 r) { tablePopupMenuItemsThreaded(table, text, _convertRunnable(r)); } void popupMenuItem_top(S text, IVF1 r) { tablePopupMenuItemsThreaded_top(table, text, _convertRunnable(r)); } // row index => element IVF1 _convertRunnable(IVF1 r) { ret r == null ?: idx -> r.get(syncGet(data, idx)); } void hideFields(S... fields) { if (hideFields == null) hideFields = new Set; _addAll(hideFields, fields); } A getRow(int row) { ret get(data, row); } void scrollDown { scrollTableDownNow(table); } void onSelect(IVF1 r) { visualize(); onTableSelectionChanged(table, -> pcallF_typed(r, selected())); } L getList() { ret data; } }