diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/DefaultTreeTableCellRenderer.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/DefaultTreeTableCellRenderer.java new file mode 100644 index 00000000..c6848408 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/DefaultTreeTableCellRenderer.java @@ -0,0 +1,121 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable; + +import java.awt.*; + +import javax.swing.*; +import javax.swing.table.TableCellRenderer; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +import com.github.weisj.darklaf.ui.tree.DarkTreeUI; +import com.github.weisj.darklaf.util.DarkUIUtil; + +public class DefaultTreeTableCellRenderer extends JComponent implements TableCellRenderer { + + private final JTreeTable treeTable; + private final RendererTree rendererTree = new RendererTree(); + private int paintingRow; + + public DefaultTreeTableCellRenderer(final JTreeTable treeTable, final TreeModel model) { + this.treeTable = treeTable; + rendererTree.setRowHeight(rendererTree.getRowHeight()); + rendererTree.setModel(model); + rendererTree.setBounds(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + @Override + protected void paintComponent(final Graphics g) { + DarkTreeUI ui = DarkUIUtil.getUIOfType(rendererTree.getUI(), DarkTreeUI.class); + if (ui != null) { + ui.paintRow(g, paintingRow); + } + } + + public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, + final boolean hasFocus, final int row, final int column) { + paintingRow = row; + rendererTree.setFocus(hasFocus || table.hasFocus()); + rendererTree.setSelectable(!hasFocus); + return this; + } + + public TreeTableTree getTree() { + return rendererTree; + } + + protected class RendererTree extends TreeTableTree { + private boolean focus; + private boolean selectable; + + public void setRowHeight(final int rowHeight) { + if (rowHeight > 0) { + super.setRowHeight(rowHeight); + if (treeTable != null && treeTable.getRowHeight() != rowHeight) { + treeTable.setRowHeight(getRowHeight()); + } + } + } + + @Override + public void repaint(final int x, final int y, final int width, final int height) { + treeTable.repaint(x, y, treeTable.getColumnModel().getColumn(0).getWidth(), height); + } + + @Override + public boolean isRowSelected(final int row) { + return selectable && super.isRowSelected(row); + } + + @Override + public boolean isPathSelected(final TreePath path) { + return selectable && super.isPathSelected(path); + } + + @Override + public void scrollRectToVisible(final Rectangle aRect) { + treeTable.scrollRectToVisible(aRect); + } + + public void setFocus(final boolean focus) { + this.focus = focus; + } + + public void setSelectable(final boolean selectable) { + this.selectable = selectable; + } + + @Override + public boolean hasFocus() { + return focus; + } + + @Override + public boolean isFocusOwner() { + return super.hasFocus(); + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/JTreeTable.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/JTreeTable.java new file mode 100644 index 00000000..28a7a5c2 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/JTreeTable.java @@ -0,0 +1,106 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable; + +import java.awt.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import javax.swing.*; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; + +import com.github.weisj.darklaf.components.treetable.model.AbstractTreeTableModel; +import com.github.weisj.darklaf.components.treetable.model.DefaultTreeTableSelectionModel; +import com.github.weisj.darklaf.components.treetable.model.TreeTableModel; +import com.github.weisj.darklaf.ui.cell.hint.CellHintPopupListener; +import com.github.weisj.darklaf.ui.tree.DarkTreeUI; +import com.github.weisj.darklaf.util.DarkUIUtil; + +public class JTreeTable extends JTable implements TreeSelectionListener { + + private final TreeTableTree tree; + + public JTreeTable(final AbstractTreeTableModel treeTableModel) { + DefaultTreeTableCellRenderer treeCellRenderer = new DefaultTreeTableCellRenderer(this, treeTableModel); + tree = treeCellRenderer.getTree(); + + add(tree); + + tree.putClientProperty(DarkTreeUI.KEY_IS_TABLE_TREE, true); + + DefaultTreeTableSelectionModel selectionModel = new DefaultTreeTableSelectionModel(tree); + tree.setSelectionModel(selectionModel); + setSelectionModel(selectionModel); + tree.addTreeSelectionListener(this); + + setDefaultRenderer(TreeTableModel.class, treeCellRenderer); + super.setModel(new TreeTableModelAdapter(treeTableModel, tree)); + setShowHorizontalLines(false); + } + + @Override + public void doLayout() { + super.doLayout(); + int grid = getShowVerticalLines() ? getIntercellSpacing().width : 0; + tree.setBounds(0, 0, getColumnModel().getColumn(0).getWidth() - grid, getHeight()); + } + + @Override + public void repaint(final Rectangle r) { + super.repaint(r); + SwingUtilities.invokeLater(() -> { + DarkTreeUI ui = DarkUIUtil.getUIOfType(tree.getUI(), DarkTreeUI.class); + if (ui != null) { + CellHintPopupListener listener = ui.getPopupListener(); + if (listener != null) listener.repaint(); + } + }); + } + + @Override + public int getComponentCount() { + return 0; + } + + @Override + protected void processEvent(final AWTEvent e) { + if (e instanceof InputEvent) { + if (e instanceof KeyEvent) { + if (getColumnModel().getSelectionModel().getLeadSelectionIndex() == 0 + && !((KeyEvent) e).isShiftDown()) { + tree.processEvent(e); + } + } + if (((InputEvent) e).isConsumed()) return; + } + super.processEvent(e); + } + + @Override + public void valueChanged(final TreeSelectionEvent e) { + repaint(0, 0, getColumnModel().getColumn(0).getWidth(), getHeight()); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableModelAdapter.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableModelAdapter.java new file mode 100644 index 00000000..4821bc54 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableModelAdapter.java @@ -0,0 +1,103 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable; + +import javax.swing.*; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeExpansionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import com.github.weisj.darklaf.components.treetable.model.TreeTableModel; + +public class TreeTableModelAdapter extends AbstractTableModel { + + private final JTree tree; + private final TreeTableModel treeTableModel; + + public TreeTableModelAdapter(final TreeTableModel treeTableModel, final JTree tree) { + this.tree = tree; + this.treeTableModel = treeTableModel; + + tree.addTreeExpansionListener(new TreeExpansionListener() { + public void treeExpanded(final TreeExpansionEvent event) { + TreePath path = event.getPath(); + int start = tree.getRowForPath(path); + int length = ((TreeNode) path.getLastPathComponent()).getChildCount(); + int selection = tree.getLeadSelectionRow(); + fireTableRowsInserted(start, start + length); + tree.setSelectionRow(selection); + } + + public void treeCollapsed(final TreeExpansionEvent event) { + TreePath path = event.getPath(); + int start = tree.getRowForPath(path); + int length = ((TreeNode) path.getLastPathComponent()).getChildCount(); + int selection = tree.getLeadSelectionRow(); + fireTableRowsDeleted(start, start + length); + tree.setSelectionRow(selection); + } + }); + } + + @Override + public void fireTableRowsDeleted(final int firstRow, final int lastRow) { + super.fireTableRowsDeleted(firstRow, lastRow); + } + + public int getColumnCount() { + return treeTableModel.getColumnCount(); + } + + public String getColumnName(final int column) { + return treeTableModel.getColumnName(column); + } + + public Class getColumnClass(final int column) { + return treeTableModel.getColumnClass(column); + } + + public int getRowCount() { + return tree.getRowCount(); + } + + protected Object nodeForRow(final int row) { + TreePath treePath = tree.getPathForRow(row); + return treePath.getLastPathComponent(); + } + + public Object getValueAt(final int row, final int column) { + return treeTableModel.getValueAt(nodeForRow(row), column); + } + + public boolean isCellEditable(final int row, final int column) { + return treeTableModel.isCellEditable(nodeForRow(row), column); + } + + public void setValueAt(final Object value, final int row, final int column) { + treeTableModel.setValueAt(value, nodeForRow(row), column); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableSelectionModel.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableSelectionModel.java new file mode 100644 index 00000000..89d811c9 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableSelectionModel.java @@ -0,0 +1,30 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable; + +import javax.swing.*; +import javax.swing.tree.TreeSelectionModel; + +public interface TreeTableSelectionModel extends TreeSelectionModel, ListSelectionModel {} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableTree.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableTree.java new file mode 100644 index 00000000..dbe3bc62 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/TreeTableTree.java @@ -0,0 +1,40 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable; + +import java.awt.*; + +import javax.swing.*; + +public class TreeTableTree extends JTree { + + @Override + public void processEvent(final AWTEvent e) { + super.processEvent(e); + } + + @Override + public void paint(final Graphics g) {} +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/AbstractTreeTableModel.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/AbstractTreeTableModel.java new file mode 100644 index 00000000..c632d5b3 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/AbstractTreeTableModel.java @@ -0,0 +1,119 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable.model; + +import javax.swing.event.EventListenerList; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreePath; + +public abstract class AbstractTreeTableModel implements TreeTableModel { + protected TreeTableNode root; + protected EventListenerList listenerList = new EventListenerList(); + + private static final int CHANGED = 0; + private static final int INSERTED = 1; + private static final int REMOVED = 2; + private static final int STRUCTURE_CHANGED = 3; + + public AbstractTreeTableModel(final TreeTableNode root) { + this.root = root; + } + + @Override + public TreeTableNode getRoot() { + return root; + } + + @Override + public boolean isLeaf(final Object node) { + return getChildCount(node) == 0; + } + + @Override + public void valueForPathChanged(final TreePath path, final Object newValue) {} + + @Override + public int getIndexOfChild(final Object parent, final Object child) { + return 0; + } + + @Override + public void addTreeModelListener(final TreeModelListener l) { + listenerList.add(TreeModelListener.class, l); + } + + @Override + public void removeTreeModelListener(final TreeModelListener l) { + listenerList.remove(TreeModelListener.class, l); + } + + private void fireTreeNode(final int changeType, final Object source, final Object[] path, final int[] childIndices, + final Object[] children) { + Object[] listeners = listenerList.getListenerList(); + TreeModelEvent e = new TreeModelEvent(source, path, childIndices, children); + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == TreeModelListener.class) { + switch (changeType) { + case CHANGED : + ((TreeModelListener) listeners[i + 1]).treeNodesChanged(e); + break; + case INSERTED : + ((TreeModelListener) listeners[i + 1]).treeNodesInserted(e); + break; + case REMOVED : + ((TreeModelListener) listeners[i + 1]).treeNodesRemoved(e); + break; + case STRUCTURE_CHANGED : + ((TreeModelListener) listeners[i + 1]).treeStructureChanged(e); + break; + default : + break; + } + + } + } + } + + protected void fireTreeNodesChanged(final Object source, final Object[] path, + final int[] childIndices, final Object[] children) { + fireTreeNode(CHANGED, source, path, childIndices, children); + } + + protected void fireTreeNodesInserted(final Object source, final Object[] path, + final int[] childIndices, final Object[] children) { + fireTreeNode(INSERTED, source, path, childIndices, children); + } + + protected void fireTreeNodesRemoved(final Object source, final Object[] path, + final int[] childIndices, final Object[] children) { + fireTreeNode(REMOVED, source, path, childIndices, children); + } + + protected void fireTreeStructureChanged(final Object source, final Object[] path, + final int[] childIndices, final Object[] children) { + fireTreeNode(STRUCTURE_CHANGED, source, path, childIndices, children); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableModel.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableModel.java new file mode 100644 index 00000000..e265cf8f --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableModel.java @@ -0,0 +1,95 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable.model; + +import com.github.weisj.darklaf.util.DarkUIUtil; + +public class DefaultTreeTableModel extends AbstractTreeTableModel { + + private final String[] headers; + private Class[] columnClasses; + + public DefaultTreeTableModel(final TreeTableNode root, final String[] headers) { + super(root); + this.headers = headers; + } + + public void setColumnClasses(final Class[] columnClasses) { + this.columnClasses = columnClasses; + } + + @Override + public int getColumnCount() { + return headers.length; + } + + @Override + public String getColumnName(final int column) { + return headers[column]; + } + + @Override + public Class getColumnClass(final int column) { + if (column == 0) { + return TreeTableModel.class; + } else if (columnClasses != null && column < columnClasses.length) { + return columnClasses[column]; + } else { + return Object.class; + } + } + + @Override + public Object getValueAt(final Object node, final int column) { + TreeTableNode treeTableNode = DarkUIUtil.nullableCast(TreeTableNode.class, node); + if (treeTableNode == null) return null; + return treeTableNode.getValueAt(column); + } + + @Override + public boolean isCellEditable(final Object node, final int column) { + return column != 0; + } + + @Override + public void setValueAt(final Object aValue, final Object node, final int column) { + TreeTableNode treeTableNode = DarkUIUtil.nullableCast(TreeTableNode.class, node); + if (treeTableNode != null) { + treeTableNode.setValueAt(column, aValue); + } + } + + @Override + public Object getChild(final Object parent, final int index) { + TreeTableNode treeTableNode = DarkUIUtil.nullableCast(TreeTableNode.class, parent); + return treeTableNode != null ? treeTableNode.getChildAt(index) : null; + } + + @Override + public int getChildCount(final Object parent) { + TreeTableNode treeTableNode = DarkUIUtil.nullableCast(TreeTableNode.class, parent); + return treeTableNode != null ? treeTableNode.getChildCount() : 0; + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableNode.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableNode.java new file mode 100644 index 00000000..cab246a2 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableNode.java @@ -0,0 +1,79 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable.model; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class DefaultTreeTableNode implements TreeTableNode { + + private final List children; + private final TreeTableNode parent; + private final List columns; + + public DefaultTreeTableNode(final TreeTableNode parent, + final Object[] columns) { + this(parent, Arrays.asList(columns)); + } + + public DefaultTreeTableNode(final TreeTableNode parent, final List columns) { + this.parent = parent; + this.columns = columns; + this.children = new ArrayList<>(); + } + + @Override + public List getChildren() { + return children; + } + + @Override + public TreeTableNode getParent() { + return parent; + } + + @Override + public boolean getAllowsChildren() { + return true; + } + + public void addChild(final TreeTableNode child) { + children.add(child); + } + + public void removeChild(final TreeTableNode child) { + children.remove(child); + } + + public String toString() { + return getTreeValue().toString(); + } + + @Override + public List getColumns() { + return columns; + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableSelectionModel.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableSelectionModel.java new file mode 100644 index 00000000..90d2e80b --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DefaultTreeTableSelectionModel.java @@ -0,0 +1,99 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable.model; + +import javax.swing.*; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreePath; + +public class DefaultTreeTableSelectionModel extends DefaultTreeSelectionModel + implements DelegatingListSelectionModel { + + private final JTree tree; + private final ListSelectionModel bridgeModel; + + public DefaultTreeTableSelectionModel(final JTree tree) { + this.tree = tree; + bridgeModel = new TreeTableListSelectionModel(); + } + + @Override + public ListSelectionModel getListDelegate() { + return bridgeModel; + } + + protected class TreeTableListSelectionModel implements DelegatingListSelectionModel { + + @Override + public ListSelectionModel getListDelegate() { + return listSelectionModel; + } + + @Override + public void setSelectionInterval(final int index0, final int index1) { + setSelectionPaths(getTreePaths(index0, index1)); + } + + @Override + public void addSelectionInterval(final int index0, final int index1) { + addSelectionPaths(getTreePaths(index0, index1)); + } + + protected TreePath[] getTreePaths(final int index0, final int index1) { + int start = Math.min(index0, index1); + int end = Math.max(index0, index1); + TreePath[] paths = new TreePath[end - start + 1]; + for (int i = start; i <= end; i++) { + paths[i - start] = tree.getPathForRow(i); + } + return paths; + } + + @Override + public void removeSelectionInterval(final int index0, final int index1) { + removeSelectionPaths(getTreePaths(index0, index1)); + } + + @Override + public void setLeadSelectionIndex(final int index) { + tree.setLeadSelectionPath(tree.getPathForRow(index)); + } + + @Override + public int getLeadSelectionIndex() { + return getLeadSelectionRow(); + } + + @Override + public void setAnchorSelectionIndex(final int index) { + tree.setAnchorSelectionPath(tree.getPathForRow(index)); + } + + @Override + public int getAnchorSelectionIndex() { + return tree.getRowForPath(tree.getAnchorSelectionPath()); + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DelegatingListSelectionModel.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DelegatingListSelectionModel.java new file mode 100644 index 00000000..b57f53a9 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/DelegatingListSelectionModel.java @@ -0,0 +1,166 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable.model; + +import javax.swing.*; +import javax.swing.event.ListSelectionListener; + +public interface DelegatingListSelectionModel extends ListSelectionModel { + ListSelectionModel getListDelegate(); + + @Override + default void setSelectionInterval(final int index0, final int index1) { + getListDelegate().setSelectionInterval(index0, index1); + } + + @Override + default void addSelectionInterval(final int index0, final int index1) { + getListDelegate().addSelectionInterval(index0, index1); + + } + + @Override + default void removeSelectionInterval(final int index0, final int index1) { + getListDelegate().removeSelectionInterval(index0, index1); + } + + @Override + default int getMinSelectionIndex() { + return getListDelegate().getMinSelectionIndex(); + } + + @Override + default int getMaxSelectionIndex() { + return getListDelegate().getMaxSelectionIndex(); + } + + @Override + default boolean isSelectedIndex(final int index) { + return getListDelegate().isSelectedIndex(index); + } + + @Override + default int getAnchorSelectionIndex() { + return getListDelegate().getAnchorSelectionIndex(); + } + + @Override + default void setAnchorSelectionIndex(final int index) { + getListDelegate().setAnchorSelectionIndex(index); + } + + @Override + default int getLeadSelectionIndex() { + return getListDelegate().getLeadSelectionIndex(); + } + + @Override + default void setLeadSelectionIndex(final int index) { + getListDelegate().setLeadSelectionIndex(index); + } + + @Override + default void clearSelection() { + getListDelegate().clearSelection(); + } + + @Override + default boolean isSelectionEmpty() { + return getListDelegate().isSelectionEmpty(); + } + + @Override + default void insertIndexInterval(final int index, final int length, final boolean before) { + getListDelegate().insertIndexInterval(index, length, before); + } + + @Override + default void removeIndexInterval(final int index0, final int index1) { + getListDelegate().removeIndexInterval(index0, index1); + } + + @Override + default void setValueIsAdjusting(final boolean valueIsAdjusting) { + getListDelegate().setValueIsAdjusting(valueIsAdjusting); + } + + @Override + default boolean getValueIsAdjusting() { + return getListDelegate().getValueIsAdjusting(); + } + + @Override + default void setSelectionMode(final int selectionMode) { + getListDelegate().setSelectionMode(selectionMode); + } + + @Override + default int getSelectionMode() { + return getListDelegate().getSelectionMode(); + } + + @Override + default void addListSelectionListener(final ListSelectionListener x) { + getListDelegate().addListSelectionListener(x); + } + + @Override + default void removeListSelectionListener(final ListSelectionListener x) { + getListDelegate().removeListSelectionListener(x); + } + + default int[] getSelectedIndices() { + int iMin = getMinSelectionIndex(); + int iMax = getMaxSelectionIndex(); + + if ((iMin < 0) || (iMax < 0)) { + return new int[0]; + } + + int[] rvTmp = new int[1 + (iMax - iMin)]; + int n = 0; + for (int i = iMin; i <= iMax; i++) { + if (isSelectedIndex(i)) { + rvTmp[n++] = i; + } + } + int[] rv = new int[n]; + System.arraycopy(rvTmp, 0, rv, 0, n); + return rv; + } + + default int getSelectedItemsCount() { + int iMin = getMinSelectionIndex(); + int iMax = getMaxSelectionIndex(); + int count = 0; + + for (int i = iMin; i <= iMax; i++) { + if (isSelectedIndex(i)) { + count++; + } + } + return count; + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/TreeTableModel.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/TreeTableModel.java new file mode 100644 index 00000000..3199ed82 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/TreeTableModel.java @@ -0,0 +1,80 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable.model; + +import javax.swing.tree.TreeModel; + +public interface TreeTableModel extends TreeModel { + + /** + * Returns the number of available columns. + * + * @return Number of Columns + */ + int getColumnCount(); + + /** + * Returns the column name. + * + * @param column Column number + * @return Column name + */ + String getColumnName(int column); + + /** + * Returns the type (class) of a column. + * + * @param column Column number + * @return Class + */ + Class getColumnClass(int column); + + /** + * Returns the value of a node in a column. + * + * @param node Node + * @param column Column number + * @return Value of the node in the column + */ + Object getValueAt(Object node, int column); + + /** + * Check if a cell of a node in one column is editable. + * + * @param node Node + * @param column Column number + * @return true/false + */ + boolean isCellEditable(Object node, int column); + + /** + * Sets a value for a node in one column. + * + * @param aValue New value + * @param node Node + * @param column Column number + */ + void setValueAt(Object aValue, Object node, int column); +} diff --git a/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/TreeTableNode.java b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/TreeTableNode.java new file mode 100644 index 00000000..e73b6ec5 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/components/treetable/model/TreeTableNode.java @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package com.github.weisj.darklaf.components.treetable.model; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +import javax.swing.tree.TreeNode; + +public interface TreeTableNode extends TreeNode { + + List getChildren(); + + @Override + default Enumeration children() { + return Collections.enumeration(getChildren()); + } + + @Override + TreeTableNode getParent(); + + @Override + default int getIndex(final TreeNode node) { + if (!(node instanceof TreeTableNode)) return -1; + return getIndex((TreeTableNode) node); + } + + default int getIndex(final TreeTableNode node) { + return getChildren().indexOf(node); + } + + @Override + default int getChildCount() { + return getChildren().size(); + } + + @Override + default TreeTableNode getChildAt(final int index) { + if (index < 0 || index >= getChildCount()) return null; + return getChildren().get(index); + } + + @Override + default boolean isLeaf() { + return getChildCount() == 0; + } + + List getColumns(); + + default Object getValueAt(final int column) { + return getColumns().get(column); + } + + default Object getTreeValue() { + return getColumns().get(0); + } + + default int getColumnCount() { + return getColumns().size(); + } + + default void setValueAt(final int column, final Object aValue) { + getColumns().set(column, aValue); + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/table/renderer/DarkTableCellRendererDelegate.java b/core/src/main/java/com/github/weisj/darklaf/ui/table/renderer/DarkTableCellRendererDelegate.java index 442db3bf..559c1fb1 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/table/renderer/DarkTableCellRendererDelegate.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/table/renderer/DarkTableCellRendererDelegate.java @@ -27,6 +27,7 @@ package com.github.weisj.darklaf.ui.table.renderer; import java.awt.*; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.table.JTableHeader; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; @@ -64,7 +65,7 @@ public class DarkTableCellRendererDelegate extends TableCellRendererDelegate imp boolean paintSelected = isSelected && !isLeadSelectionCell && !table.isEditing(); if (component instanceof JComponent) { - setupBorderStyle(table, row, column, (JComponent) component, isRowFocus); + setupBorderStyle(table, row, column, (JComponent) component, isLeadSelectionCell, isRowFocus); } CellUtil.setupTableForeground(component, table, paintSelected); CellUtil.setupTableBackground(component, table, paintSelected, row); @@ -72,26 +73,32 @@ public class DarkTableCellRendererDelegate extends TableCellRendererDelegate imp } public void setupBorderStyle(final JTable table, final int row, final int column, - final JComponent component, final boolean isRowFocus) { - if (isRowFocus + final JComponent component, final boolean isLeadSelectionCell, + final boolean isRowFocus) { + Border focusBorder = UIManager.getBorder("Table.focusSelectedCellHighlightBorder"); + if (isLeadSelectionCell && table.getSelectionModel().getLeadSelectionIndex() == row && DarkUIUtil.hasFocus(table) && !table.isEditing()) { - LookAndFeel.installBorder(component, "Table.focusSelectedCellHighlightBorder"); - component.putClientProperty(KEY_FULL_ROW_FOCUS_BORDER, true); - JTableHeader header = table.getTableHeader(); - TableColumn draggedColumn = (header == null) ? null : header.getDraggedColumn(); - boolean forceLeft = false; - boolean forceRight = false; - if (draggedColumn != null) { - int index = DarkTableUI.viewIndexForColumn(draggedColumn, table); - forceLeft = column == index + 1 || column == index; - forceRight = column == index - 1 || column == index; + PropertyUtil.installBorder(component, focusBorder); + if (isRowFocus) { + component.putClientProperty(KEY_FULL_ROW_FOCUS_BORDER, true); + JTableHeader header = table.getTableHeader(); + TableColumn draggedColumn = (header == null) ? null : header.getDraggedColumn(); + boolean forceLeft = false; + boolean forceRight = false; + if (draggedColumn != null) { + int index = DarkTableUI.viewIndexForColumn(draggedColumn, table); + forceLeft = column == index + 1 || column == index; + forceRight = column == index - 1 || column == index; + } + component.putClientProperty(KEY_FORCE_RIGHT_BORDER, forceRight); + component.putClientProperty(KEY_FORCE_LEFT_BORDER, forceLeft); + } else { + component.putClientProperty(KEY_FULL_ROW_FOCUS_BORDER, false); } - component.putClientProperty(KEY_FORCE_RIGHT_BORDER, forceRight); - component.putClientProperty(KEY_FORCE_LEFT_BORDER, forceLeft); - } else { - component.putClientProperty(KEY_FULL_ROW_FOCUS_BORDER, false); + } else if (component.getBorder() == focusBorder) { + component.setBorder(null); } } diff --git a/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java index d02b869b..d497f9b7 100644 --- a/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java +++ b/core/src/main/java/com/github/weisj/darklaf/ui/tree/DarkTreeUI.java @@ -34,7 +34,10 @@ import javax.swing.*; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicTreeUI; -import javax.swing.tree.*; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellEditor; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreePath; import com.github.weisj.darklaf.graphics.PaintUtil; import com.github.weisj.darklaf.ui.cell.CellConstants; @@ -62,6 +65,9 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C public static final String STYLE_NONE = "none"; public static final String KEY_IS_TREE_EDITOR = "JComponent.isTreeEditor"; public static final String KEY_IS_TREE_RENDERER = "JComponent.isTreeRenderer"; + public static final String KEY_IS_TABLE_TREE = "JComponent.isTableTree"; + + protected static final Rectangle boundsBuffer = new Rectangle(); protected MouseListener selectionListener; protected Color lineColor; @@ -168,34 +174,33 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C tree.putClientProperty(KEY_MAC_ACTIONS_INSTALLED, Boolean.TRUE); - final InputMap inputMap = tree.getInputMap(JComponent.WHEN_FOCUSED); - inputMap.put(KeyStroke.getKeyStroke("pressed LEFT"), "collapse_or_move_up"); - inputMap.put(KeyStroke.getKeyStroke("pressed RIGHT"), "expand_or_move_down"); - inputMap.put(KeyStroke.getKeyStroke("pressed DOWN"), "move_down"); - inputMap.put(KeyStroke.getKeyStroke("pressed UP"), "move_up"); - inputMap.put(KeyStroke.getKeyStroke("pressed ENTER"), "toggle_edit"); + installInputMap(tree.getInputMap(JComponent.WHEN_FOCUSED)); final ActionMap actionMap = tree.getActionMap(); - final Action expandAction = actionMap.get("expand"); - if (expandAction != null) { - actionMap.put("expand_or_move_down", new TreeUIAction() { - @Override - public void actionPerformed(final ActionEvent e) { - JTree tree = getTree(e); - if (tree == null) return; - int selectionRow = tree.getLeadSelectionRow(); - if (selectionRow == -1) return; - - if (isLeaf(selectionRow) || tree.isExpanded(selectionRow)) { + actionMap.put("expand_or_move_down", new TreeUIAction() { + @Override + public void actionPerformed(final ActionEvent e) { + JTree tree = getTree(e); + if (tree == null) return; + int selectionRow = tree.getLeadSelectionRow(); + if (selectionRow == -1) return; + + if (isLeaf(selectionRow) || tree.isExpanded(selectionRow)) { + if (!PropertyUtil.getBooleanProperty(tree, KEY_IS_TABLE_TREE)) { moveTo(tree, selectionRow + 1); - } else { - expandAction.actionPerformed(e); } - tree.repaint(); + } else { + tree.expandRow(selectionRow); } - }); - } + tree.repaint(); + } + + @Override + public boolean accept(final Object sender) { + return acceptExpandCollapseAction(sender, false); + } + }); actionMap.put("collapse_or_move_up", new TreeUIAction() { @Override @@ -206,12 +211,19 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C if (selectionRow == -1) return; if (isLeaf(selectionRow) || tree.isCollapsed(selectionRow)) { - moveTo(tree, selectionRow - 1); + if (!PropertyUtil.getBooleanProperty(tree, KEY_IS_TABLE_TREE)) { + moveTo(tree, selectionRow - 1); + } } else { tree.collapseRow(selectionRow); } tree.repaint(); } + + @Override + public boolean accept(final Object sender) { + return acceptExpandCollapseAction(sender, true); + } }); actionMap.put("move_down", new TreeUIAction() { @@ -236,6 +248,28 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C }); } + protected boolean acceptExpandCollapseAction(final Object sender, final boolean collapsed) { + JTree tree = DarkUIUtil.nullableCast(JTree.class, sender); + if (tree == null) return false; + int selectionRow = tree.getLeadSelectionRow(); + if (selectionRow == -1) return false; + final boolean collapsedOrExpanded = collapsed + ? tree.isCollapsed(selectionRow) + : tree.isExpanded(selectionRow); + if (isLeaf(selectionRow) || collapsedOrExpanded) { + return !PropertyUtil.getBooleanProperty(tree, KEY_IS_TABLE_TREE); + } + return true; + } + + protected void installInputMap(final InputMap inputMap) { + inputMap.put(KeyStroke.getKeyStroke("pressed LEFT"), "collapse_or_move_up"); + inputMap.put(KeyStroke.getKeyStroke("pressed RIGHT"), "expand_or_move_down"); + inputMap.put(KeyStroke.getKeyStroke("pressed DOWN"), "move_down"); + inputMap.put(KeyStroke.getKeyStroke("pressed UP"), "move_up"); + inputMap.put(KeyStroke.getKeyStroke("pressed ENTER"), "toggle_edit"); + } + protected JTree getTree(final ActionEvent e) { return DarkUIUtil.nullableCast(JTree.class, e.getSource()); } @@ -268,29 +302,6 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C tree.scrollRectToVisible(bounds); } - protected void collapse(final JTree tree) { - int selectionRow = tree.getLeadSelectionRow(); - if (selectionRow == -1) return; - - TreePath selectionPath = tree.getPathForRow(selectionRow); - if (selectionPath == null) return; - - if (tree.getModel().isLeaf(selectionPath.getLastPathComponent()) - || tree.isCollapsed(selectionRow)) { - final TreePath parentPath = tree.getPathForRow(selectionRow).getParentPath(); - if (parentPath != null) { - if (parentPath.getParentPath() != null || tree.isRootVisible()) { - final int parentRow = tree.getRowForPath(parentPath); - scrollRowToVisible(tree, parentRow); - tree.setSelectionRow(parentRow); - } - } - } else { - tree.collapseRow(selectionRow); - } - tree.repaint(); - } - protected void toggleEdit(final JTree tree) { if (tree == null) return; if (tree.isEditing()) { @@ -394,97 +405,14 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C Insets insets = tree.getInsets(); TreePath initialPath = getClosestPathForLocation(tree, 0, paintBounds.y); Enumeration paintingEnumerator = treeState.getVisiblePathsFrom(initialPath); - int row = treeState.getRowForPath(initialPath); - int endY = paintBounds.y + paintBounds.height; - - drawingCache.clear(); if (initialPath != null && paintingEnumerator != null) { - TreePath parentPath = initialPath; - - // Draw the lines, knobs, and rows + int row = treeState.getRowForPath(initialPath); boolean done = false; - // Information for the node being rendered. - boolean isExpanded; - boolean hasBeenExpanded; - boolean isLeaf; - Rectangle boundsBuffer = new Rectangle(); - Rectangle bounds; - TreePath path; - boolean rootVisible = isRootVisible(); - - final int containerWidth = tree.getParent() instanceof JViewport - ? tree.getParent().getWidth() - : tree.getWidth(); - final int xOffset = tree.getParent() instanceof JViewport - ? ((JViewport) tree.getParent()).getViewPosition().x - : 0; - - // Paint row backgrounds - Enumeration backgroundEnumerator = treeState.getVisiblePathsFrom(initialPath); - while (backgroundEnumerator != null && backgroundEnumerator.hasMoreElements()) { - path = (TreePath) backgroundEnumerator.nextElement(); - if (path != null) { - bounds = getPathBounds(path, insets, boundsBuffer); - if (bounds == null) return; - bounds.x = xOffset; - bounds.width = containerWidth; - if (paintBounds.intersects(bounds)) { - paintRowBackground(g, paintBounds, bounds, path, tree.getRowForPath(path)); - } - } - } - - // Find each parent and have them draw a line to their last child - while (parentPath != null) { - paintVerticalPartOfLeg(g, paintBounds, insets, parentPath); - drawingCache.put(parentPath, Boolean.TRUE); - parentPath = parentPath.getParentPath(); - } - while (!done && paintingEnumerator.hasMoreElements()) { - path = (TreePath) paintingEnumerator.nextElement(); - if (path != null) { - isLeaf = treeModel.isLeaf(path.getLastPathComponent()); - if (isLeaf) { - isExpanded = hasBeenExpanded = false; - } else { - isExpanded = treeState.getExpandedState(path); - hasBeenExpanded = tree.hasBeenExpanded(path); - } - bounds = getPathBounds(path, insets, boundsBuffer); - if (bounds == null) - // This will only happen if the model changes out - // from under us (usually in another thread). - // Swing isn't multithreaded, but I'll put this - // check in anyway. - { - return; - } - - // See if the vertical line to the parent has been drawn. - parentPath = path.getParentPath(); - if (parentPath != null) { - if (drawingCache.get(parentPath) == null) { - paintVerticalPartOfLeg(g, paintBounds, insets, parentPath); - drawingCache.put(parentPath, Boolean.TRUE); - } - paintHorizontalPartOfLeg(g, paintBounds, insets, bounds, path, row, isExpanded, - hasBeenExpanded, isLeaf); - } else if (rootVisible && row == 0) { - paintHorizontalPartOfLeg(g, paintBounds, insets, bounds, path, row, isExpanded, - hasBeenExpanded, isLeaf); - } - if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded, isLeaf)) { - paintExpandControl(g, paintBounds, insets, bounds, path, row, isExpanded, - hasBeenExpanded, isLeaf); - } - paintRow(g, paintBounds, insets, bounds, path, row, isExpanded, hasBeenExpanded, isLeaf); - if ((bounds.y + bounds.height) >= endY) { - done = true; - } - } else { + TreePath path = (TreePath) paintingEnumerator.nextElement(); + if (!paintSingleRow(g, paintBounds, insets, path, row)) { done = true; } row++; @@ -493,34 +421,54 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C paintDropLine(g); // Empty out the renderer pane, allowing renderers to be gc'ed. rendererPane.removeAll(); - drawingCache.clear(); } - @Override - public TreeCellRenderer getCellRenderer() { - return super.getCellRenderer(); + public void paintRow(final Graphics g, final int row) { + TreePath path = getPathForRow(tree, row); + Rectangle paintBounds = g.getClipBounds(); + Insets insets = tree.getInsets(); + Rectangle rowBounds = getPathBounds(path, insets, boundsBuffer); + g.translate(0, -rowBounds.y); + paintBounds.y += rowBounds.y; + paintSingleRow(g, paintBounds, insets, path, row); + rendererPane.removeAll(); + g.translate(0, rowBounds.y); } - public Component getEditingComponent() { - return editingComponent; - } + protected boolean paintSingleRow(final Graphics g, final Rectangle paintBounds, final Insets insets, + final TreePath path, final int row) { + if (path == null) return false; + final int xOffset = tree.getParent() instanceof JViewport + ? ((JViewport) tree.getParent()).getViewPosition().x + : 0; + final int containerWidth = tree.getParent() instanceof JViewport + ? tree.getParent().getWidth() + : tree.getWidth(); + final Rectangle cellBounds = getPathBounds(path, insets, boundsBuffer); + if (cellBounds == null) return false; + final int boundsX = cellBounds.x; + final int boundsWidth = cellBounds.width; - public int getEditingRow() { - return editingRow; - } + cellBounds.x = xOffset; + cellBounds.width = containerWidth; + paintRowBackground(g, paintBounds, cellBounds, path, row); + cellBounds.x = boundsX; + cellBounds.width = boundsWidth; - protected Rectangle getPathBounds(final TreePath path, final Insets insets, Rectangle bounds) { - bounds = treeState.getBounds(path, bounds); - if (bounds != null) { - if (tree.getComponentOrientation().isLeftToRight()) { - bounds.x += insets.left; - } else { - bounds.x = tree.getWidth() - (bounds.x + bounds.width) - - insets.right; - } - bounds.y += insets.top; + if (path.getParentPath() != null) { + paintVerticalLegs(g, paintBounds, cellBounds, insets, path); } - return bounds; + + boolean isLeaf = treeModel.isLeaf(path.getLastPathComponent()); + boolean isExpanded = !isLeaf && treeState.getExpandedState(path); + boolean hasBeenExpanded = !isLeaf && tree.hasBeenExpanded(path); + + if (shouldPaintExpandControl(path, row, isExpanded, hasBeenExpanded, isLeaf)) { + paintExpandControl(g, paintBounds, insets, cellBounds, path, row, isExpanded, + hasBeenExpanded, isLeaf); + } + paintRow(g, paintBounds, insets, cellBounds, path, row, isExpanded, hasBeenExpanded, isLeaf); + return (cellBounds.y + cellBounds.height) < paintBounds.y + paintBounds.height; } protected void paintRowBackground(final Graphics g, final Rectangle clipBounds, @@ -541,12 +489,76 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C } } + /* + * Paint all vertical legs for the whole tree in this row. + */ + protected void paintVerticalLegs(final Graphics g, final Rectangle clipBounds, + final Rectangle rowBounds, + final Insets insets, final TreePath path) { + if (!shouldPaintLines()) return; + int depth = path.getPathCount() - 1; + if (depth == 0 && (!isRootVisible() || !getShowsRootHandles())) { + // Parent is the root, which isn't visible. + return; + } + int clipLeft = clipBounds.x; + int clipRight = clipBounds.x + (clipBounds.width - 1); + + TreePath parentPath = path; + for (int currentDepth = depth - 1; currentDepth >= 0; currentDepth--) { + if (currentDepth == 0 && !isRootVisible()) continue; + + int lineX = getRowX(-1, currentDepth); + if (tree.getComponentOrientation().isLeftToRight()) { + lineX = lineX - getRightChildIndent() + insets.left; + } else { + lineX = tree.getWidth() - lineX - insets.right + getRightChildIndent() - 1; + } + + if (lineX > clipRight || lineX < clipLeft) continue; + + parentPath = parentPath.getParentPath(); + g.setColor(getLineColor(parentPath)); + paintVerticalLine(g, tree, lineX, rowBounds.y, rowBounds.y + rowBounds.height); + } + } + + @Override + public TreeCellRenderer getCellRenderer() { + return super.getCellRenderer(); + } + + public Component getEditingComponent() { + return editingComponent; + } + + public int getEditingRow() { + return editingRow; + } + + protected Rectangle getPathBounds(final TreePath path, final Insets insets, Rectangle bounds) { + bounds = treeState.getBounds(path, bounds); + if (bounds != null) { + if (tree.getComponentOrientation().isLeftToRight()) { + bounds.x += insets.left; + } else { + bounds.x = tree.getWidth() - (bounds.x + bounds.width) - insets.right; + } + bounds.y += insets.top; + } + return bounds; + } + @Override public void update(final Graphics g, final JComponent c) { if (popupListener != null) popupListener.repaint(); super.update(g, c); } + public CellHintPopupListener getPopupListener() { + return popupListener; + } + @Override protected void updateRenderer() { super.updateRenderer(); @@ -628,89 +640,14 @@ public class DarkTreeUI extends BasicTreeUI implements PropertyChangeListener, C @Override protected void paintVerticalPartOfLeg(final Graphics g, final Rectangle clipBounds, - final Insets insets, final TreePath path) { - if (!shouldPaintLines()) return; - int depth = path.getPathCount() - 1; - if (depth == 0 && !getShowsRootHandles() && !isRootVisible()) { - return; - } - if (treeModel.isLeaf(path.getLastPathComponent()) || !tree.isExpanded(path)) return; - - int clipLeft = clipBounds.x; - int clipRight = clipBounds.x + (clipBounds.width - 1); - int clipTop = clipBounds.y; - int clipBottom = clipBounds.y + tree.getHeight(); - - Rectangle parentBounds = getPathBounds(tree, path); - - int top; - int bottom; - int lineX = getRowX(-1, depth); - - if (tree.getComponentOrientation().isLeftToRight()) { - lineX = lineX - getRightChildIndent() + insets.left; - } else { - lineX = tree.getWidth() - lineX - insets.right + getRightChildIndent() - 1; - } - - if (lineX > clipRight || lineX < clipLeft) return; - - if (parentBounds == null) { - top = Math.max(insets.top + getVerticalLegBuffer(), clipTop); - } else { - top = Math.max(parentBounds.y + parentBounds.height + getVerticalLegBuffer(), clipTop); - } - - if (depth == 0 && !isRootVisible()) { - TreeModel model = getModel(); - - if (model != null) { - Object root = model.getRoot(); - - if (model.getChildCount(root) > 0) { - parentBounds = getPathBounds(tree, path.pathByAddingChild(model.getChild(root, 0))); - if (parentBounds != null) { - top = Math.max(insets.top + getVerticalLegBuffer(), parentBounds.y + parentBounds.height / 2); - } - } - } - } - - int childCount = treeModel.getChildCount(path.getLastPathComponent()); - g.setColor(getLineColor(path)); - for (int i = 0; i < childCount - 1; i++) { - TreePath childPath = path.pathByAddingChild(treeModel.getChild(path.getLastPathComponent(), i)); - Rectangle childBounds = getPathBounds(tree, childPath); - if (childBounds != null) { - bottom = Math.min(childBounds.y + childBounds.height, clipBottom); - paintVerticalLine(g, tree, lineX, top, bottom); - top = bottom; - if (clipBottom < top) return; - } - } - - // Descend to deepest last child. - TreePath lastChildPath = path.pathByAddingChild(treeModel.getChild(path.getLastPathComponent(), - childCount - 1)); - while (tree.isExpanded(lastChildPath)) { - int count = treeModel.getChildCount(lastChildPath.getLastPathComponent()); - if (count == 0) break; - lastChildPath = lastChildPath.pathByAddingChild(treeModel.getChild(lastChildPath.getLastPathComponent(), - count - 1)); - } - Rectangle childBounds = getPathBounds(tree, lastChildPath); - if (childBounds != null) { - bottom = Math.min(childBounds.y + childBounds.height, clipBottom); - paintVerticalLine(g, tree, lineX, top, bottom); - } - } + final Insets insets, final TreePath path) {} @Override protected void paintExpandControl(final Graphics g, final Rectangle clipBounds, final Insets insets, final Rectangle bounds, final TreePath path, final int row, final boolean isExpanded, final boolean hasBeenExpanded, final boolean isLeaf) { if (!isLeaf(row)) { - boolean isPathSelected = tree.getSelectionModel().isPathSelected(path); + boolean isPathSelected = tree.isPathSelected(path); setExpandedIcon(getExpandedIcon(isPathSelected, tree.hasFocus() || tree.isEditing())); setCollapsedIcon(getCollapsedIcon(isPathSelected, tree.hasFocus() || tree.isEditing())); } diff --git a/core/src/test/java/ui/tree/TreeDemo.java b/core/src/test/java/ui/tree/TreeDemo.java index 68f011bd..b446e3eb 100644 --- a/core/src/test/java/ui/tree/TreeDemo.java +++ b/core/src/test/java/ui/tree/TreeDemo.java @@ -54,12 +54,23 @@ public class TreeDemo implements ComponentDemo { DefaultMutableTreeNode child1 = new SelectableTreeNode("Leaf B (boolean)", true); DefaultMutableTreeNode parent2 = new DefaultMutableTreeNode("Node B"); DefaultMutableTreeNode child2 = new DefaultMutableTreeNode("Leaf that is unnecessary verbose and ridiculously long C"); + DefaultMutableTreeNode parent3 = new DefaultMutableTreeNode("Nested"); + + DefaultMutableTreeNode current = parent3; + for (int i = 0; i < 10; i++) { + DefaultMutableTreeNode node1 = new DefaultMutableTreeNode("Nested1 " + i); + DefaultMutableTreeNode node2 = new DefaultMutableTreeNode("Nested2 " + i); + current.add(node1); + current.add(node2); + current = node1; + } parent1.add(child); parent1.add(child1); parent2.add(child2); root.add(parent1); root.add(parent2); + root.add(parent3); for (int i = 0; i < 100; i++) { root.add(new DefaultMutableTreeNode("Leaf " + i)); @@ -103,6 +114,12 @@ public class TreeDemo implements ComponentDemo { : ComponentOrientation.RIGHT_TO_LEFT)); } }); + controlPanel.add(new JCheckBox("show root") { + { + setSelected(tree.isRootVisible()); + addActionListener(e -> tree.setRootVisible(isSelected())); + } + }); controlPanel.add(new JCheckBox("show root handles") { { setSelected(tree.getShowsRootHandles()); diff --git a/core/src/test/java/ui/treetable/TreeTableDemo.java b/core/src/test/java/ui/treetable/TreeTableDemo.java new file mode 100644 index 00000000..c68ceee3 --- /dev/null +++ b/core/src/test/java/ui/treetable/TreeTableDemo.java @@ -0,0 +1,96 @@ +/* + * MIT License + * + * Copyright (c) 2020 Jannis Weis + * + * 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. + * + */ +package ui.treetable; + +import java.awt.*; +import java.util.Date; +import java.util.Random; + +import javax.swing.*; + +import com.github.weisj.darklaf.components.treetable.JTreeTable; +import com.github.weisj.darklaf.components.treetable.model.*; + +import ui.ComponentDemo; +import ui.DemoPanel; + +public class TreeTableDemo implements ComponentDemo { + + public static void main(final String[] args) { + ComponentDemo.showDemo(new TreeTableDemo()); + } + + @Override + public JComponent createComponent() { + AbstractTreeTableModel treeTableModel = new DemoModel(createDataStructure()); + JTreeTable treeTable = new JTreeTable(treeTableModel); + return new DemoPanel(new JScrollPane(treeTable), new BorderLayout(), 0); + } + + private DemoNode createDataStructure() { + Random r = new Random(); + DemoNode root = new DemoNode(null, "R1", "R1", new Date(r.nextLong()), 10); + for (int i = 0; i < 16; i++) { + DefaultTreeTableNode node = new DemoNode(root, "N" + i, "C1", new Date(r.nextLong()), 10); + if (i % 2 == 0) { + node.addChild(new DemoNode(node, "N12N12N12", "C12", new Date(r.nextLong()), 50)); + node.addChild(new DemoNode(node, "N13N12N12", "C13", new Date(r.nextLong()), 60)); + node.addChild(new DemoNode(node, "N14N12N12", "C14", new Date(r.nextLong()), 70)); + node.addChild(new DemoNode(node, "N15N12N12", "C15", new Date(r.nextLong()), 80)); + } else { + node.addChild(new DemoNode(node, "N12N12N12", "C12", new Date(r.nextLong()), 10)); + node.addChild(new DemoNode(node, "N13N12N12", "C13", new Date(r.nextLong()), 20)); + node.addChild(new DemoNode(node, "N14N12N12", "C14", new Date(r.nextLong()), 30)); + node.addChild(new DemoNode(node, "N15N12N12", "C15", new Date(r.nextLong()), 40)); + } + root.addChild(node); + } + return root; + } + + @Override + public String getTitle() { + return "TreeTable Demo"; + } + + protected static class DemoNode extends DefaultTreeTableNode { + + public DemoNode(final TreeTableNode parent, final String name, final String capital, + final Date declared, final Integer area) { + super(parent, new Object[]{name, capital, declared, area}); + } + } + + protected static class DemoModel extends DefaultTreeTableModel { + static protected String[] columnNames = {"TreeNode", "String", "Date", "Integer"}; + static protected Class[] columnTypes = {TreeTableModel.class, String.class, Date.class, Integer.class}; + + public DemoModel(final TreeTableNode rootNode) { + super(rootNode, columnNames); + setColumnClasses(columnTypes); + root = rootNode; + } + } +} diff --git a/utils/src/main/java/com/github/weisj/darklaf/util/PropertyUtil.java b/utils/src/main/java/com/github/weisj/darklaf/util/PropertyUtil.java index 229c2a77..38a2eca8 100644 --- a/utils/src/main/java/com/github/weisj/darklaf/util/PropertyUtil.java +++ b/utils/src/main/java/com/github/weisj/darklaf/util/PropertyUtil.java @@ -31,6 +31,7 @@ import java.util.Objects; import java.util.stream.Collectors; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.plaf.UIResource; public class PropertyUtil { @@ -51,6 +52,14 @@ public class PropertyUtil { } } + public static void installBorder(final JComponent component, final Border border) { + if (component == null) return; + Border b = component.getBorder(); + if (b == null || b instanceof UIResource) { + component.setBorder(border); + } + } + public static void installProperty(final JComponent c, final String key, final Object value) { if (c.getClientProperty(key) == null) { c.putClientProperty(key, value);