/******************************************************************************* * Copyright (c) 2015 Jeff Martin. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public * License v3.0 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * *

Contributors: * Jeff Martin - initial API and implementation ******************************************************************************/ package cuchaz.enigma.gui; import java.awt.Component; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.EventObject; import java.util.List; import javax.swing.JTree; import javax.swing.event.CellEditorListener; import javax.swing.event.ChangeEvent; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellEditor; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import cuchaz.enigma.gui.node.ClassSelectorClassNode; import cuchaz.enigma.gui.util.GuiUtil; import cuchaz.enigma.translation.representation.entry.ClassEntry; import cuchaz.enigma.utils.validation.ValidationContext; public class ClassSelector extends JTree { public static final Comparator DEOBF_CLASS_COMPARATOR = Comparator.comparing(ClassEntry::getFullName); private final Comparator comparator; private final GuiController controller; private NestedPackages packageManager; private ClassSelectionListener selectionListener; private RenameSelectionListener renameSelectionListener; public ClassSelector(Gui gui, Comparator comparator, boolean isRenamable) { this.comparator = comparator; this.controller = gui.getController(); // configure the tree control setEditable(true); setRootVisible(false); setShowsRootHandles(false); setModel(null); // hook events addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent event) { if (selectionListener != null && event.getClickCount() == 2) { // get the selected node TreePath path = getSelectionPath(); if (path != null && path.getLastPathComponent() instanceof ClassSelectorClassNode node) { selectionListener.onSelectClass(node.getObfEntry()); } } } }); addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { TreePath[] paths = getSelectionPaths(); if (paths != null) { if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_O) { for (TreePath path : paths) { if (path.getLastPathComponent() instanceof ClassSelectorClassNode node) { gui.toggleMappingFromEntry(node.getObfEntry()); } } } if (selectionListener != null && e.getKeyCode() == KeyEvent.VK_ENTER) { for (TreePath path : paths) { if (path.getLastPathComponent() instanceof ClassSelectorClassNode node) { selectionListener.onSelectClass(node.getObfEntry()); } } } } } }); final DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer() { { setLeafIcon(GuiUtil.CLASS_ICON); } @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (leaf && value instanceof ClassSelectorClassNode) { setIcon(GuiUtil.getClassIcon(gui, ((ClassSelectorClassNode) value).getObfEntry())); } return this; } }; setCellRenderer(renderer); final JTree tree = this; final DefaultTreeCellEditor editor = new DefaultTreeCellEditor(tree, renderer) { @Override public boolean isCellEditable(EventObject event) { return isRenamable && !(event instanceof MouseEvent) && super.isCellEditable(event); } }; this.setCellEditor(editor); editor.addCellEditorListener(new CellEditorListener() { @Override public void editingStopped(ChangeEvent e) { String data = editor.getCellEditorValue().toString(); TreePath path = getSelectionPath(); Object realPath = path.getLastPathComponent(); if (realPath instanceof DefaultMutableTreeNode node && data != null) { TreeNode parentNode = node.getParent(); if (parentNode == null) { return; } boolean allowEdit = true; for (int i = 0; i < parentNode.getChildCount(); i++) { TreeNode childNode = parentNode.getChildAt(i); if (childNode != null && childNode.toString().equals(data) && childNode != node) { allowEdit = false; break; } } if (allowEdit && renameSelectionListener != null) { Object prevData = node.getUserObject(); Object objectData = node.getUserObject() instanceof ClassEntry ? new ClassEntry(((ClassEntry) prevData).getPackageName() + "/" + data) : data; gui.validateImmediateAction(vc -> { renameSelectionListener.onSelectionRename(vc, node.getUserObject(), objectData, node); if (vc.canProceed()) { node.setUserObject(objectData); // Make sure that it's modified } else { editor.cancelCellEditing(); } }); } else { editor.cancelCellEditing(); } } } @Override public void editingCanceled(ChangeEvent e) { // NOP } }); // init defaults this.selectionListener = null; this.renameSelectionListener = null; } public void setSelectionListener(ClassSelectionListener val) { this.selectionListener = val; } public void setRenameSelectionListener(RenameSelectionListener renameSelectionListener) { this.renameSelectionListener = renameSelectionListener; } public void setClasses(Collection classEntries) { List state = getExpansionState(); if (classEntries == null) { setModel(null); return; } // update the tree control packageManager = new NestedPackages(classEntries, comparator, controller.project.getMapper()); setModel(new DefaultTreeModel(packageManager.getRoot())); restoreExpansionState(state); } public ClassEntry getSelectedClass() { if (!isSelectionEmpty()) { Object selectedNode = getSelectionPath().getLastPathComponent(); if (selectedNode instanceof ClassSelectorClassNode classNode) { return classNode.getClassEntry(); } } return null; } public enum State { EXPANDED, SELECTED } public record StateEntry(State state, TreePath path) { } public List getExpansionState() { List state = new ArrayList<>(); int rowCount = getRowCount(); for (int i = 0; i < rowCount; i++) { TreePath path = getPathForRow(i); if (isPathSelected(path)) { state.add(new StateEntry(State.SELECTED, path)); } if (isExpanded(path)) { state.add(new StateEntry(State.EXPANDED, path)); } } return state; } public void restoreExpansionState(List expansionState) { clearSelection(); for (StateEntry entry : expansionState) { switch (entry.state) { case SELECTED -> addSelectionPath(entry.path); case EXPANDED -> expandPath(entry.path); } } } public void expandPackage(String packageName) { if (packageName == null) { return; } expandPath(packageManager.getPackagePath(packageName)); } public void expandAll() { for (DefaultMutableTreeNode packageNode : packageManager.getPackageNodes()) { expandPath(new TreePath(packageNode.getPath())); } } public void collapseAll() { // sort the package nodes by depth, so we collapse the deepest nodes first List packageNodes = new ArrayList<>(packageManager.getPackageNodes()); packageNodes.sort(Comparator.comparingInt(DefaultMutableTreeNode::getDepth)); // collapse the nodes for (DefaultMutableTreeNode packageNode : packageNodes) { collapsePath(new TreePath(packageNode.getPath())); } } public void setSelectionClass(ClassEntry classEntry) { expandPackage(classEntry.getPackageName()); ClassSelectorClassNode node = packageManager.getClassNode(classEntry); if (node != null) { TreePath path = new TreePath(node.getPath()); setSelectionPath(path); scrollPathToVisible(path); } } public void moveClassIn(ClassEntry classEntry) { removeEntry(classEntry); packageManager.addEntry(classEntry); } public void removeEntry(ClassEntry classEntry) { packageManager.removeClassNode(classEntry); } public void reload() { DefaultTreeModel model = (DefaultTreeModel) getModel(); model.reload(packageManager.getRoot()); } public interface ClassSelectionListener { void onSelectClass(ClassEntry classEntry); } public interface RenameSelectionListener { void onSelectionRename(ValidationContext vc, Object prevData, Object data, DefaultMutableTreeNode node); } }