// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.gui; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Font; import java.awt.GridLayout; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Locale; import java.util.Stack; import javax.swing.JButton; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.SwingConstants; import javax.swing.Timer; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.infinity.NearInfinity; import org.infinity.icon.Icons; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.key.BIFFResourceEntry; import org.infinity.resource.key.FileResourceEntry; import org.infinity.resource.key.ResourceEntry; import org.infinity.resource.key.ResourceTreeFolder; import org.infinity.resource.key.ResourceTreeModel; public final class ResourceTree extends JPanel implements TreeSelectionListener, ActionListener { private final JButton bnext = new JButton("Forward", Icons.getIcon(Icons.ICON_FORWARD_16)); private final JButton bprev = new JButton("Back", Icons.getIcon(Icons.ICON_BACK_16)); private final JTree tree = new JTree(); private final Stack<ResourceEntry> nextstack = new Stack<ResourceEntry>(); private final Stack<ResourceEntry> prevstack = new Stack<ResourceEntry>(); private ResourceEntry prevnextnode, shownresource; private boolean showresource = true; public ResourceTree(ResourceTreeModel treemodel) { tree.setCellRenderer(new ResourceTreeRenderer()); tree.addKeyListener(new TreeKeyListener()); tree.addMouseListener(new TreeMouseListener()); tree.setModel(treemodel); tree.putClientProperty("JTree.lineStyle", "Angled"); tree.clearSelection(); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setRootVisible(false); tree.addTreeSelectionListener(this); tree.setShowsRootHandles(true); bnext.addActionListener(this); bprev.addActionListener(this); bnext.setEnabled(false); bprev.setEnabled(false); bnext.setHorizontalTextPosition(SwingConstants.LEADING); bnext.setMargin(new Insets(3, 1, 3, 1)); bprev.setMargin(bnext.getMargin()); JPanel panel = new JPanel(); panel.setLayout(new GridLayout(1, 2)); panel.add(bprev); panel.add(bnext); setLayout(new BorderLayout()); add(new JScrollPane(tree), BorderLayout.CENTER); add(panel, BorderLayout.SOUTH); } // --------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == bprev) { nextstack.push(prevnextnode); prevnextnode = prevstack.pop(); bnext.setEnabled(true); bprev.setEnabled(!prevstack.empty()); select(prevnextnode); } else if (event.getSource() == bnext) { prevstack.push(prevnextnode); prevnextnode = nextstack.pop(); bprev.setEnabled(true); bnext.setEnabled(!nextstack.empty()); select(prevnextnode); } } // --------------------- End Interface ActionListener --------------------- // --------------------- Begin Interface TreeSelectionListener --------------------- @Override public void valueChanged(TreeSelectionEvent event) { Object node = tree.getLastSelectedPathComponent(); if (node == null) { tree.clearSelection(); BrowserMenuBar.getInstance().resourceEntrySelected(null); } else if (node instanceof ResourceEntry) { ResourceEntry entry = (ResourceEntry)node; BrowserMenuBar.getInstance().resourceEntrySelected((ResourceEntry)node); if (entry != prevnextnode) { // Not result of pressing 'Back' or 'Forward' if (prevnextnode != null) { prevstack.push(prevnextnode); bprev.setEnabled(true); } nextstack.removeAllElements(); bnext.setEnabled(false); prevnextnode = entry; } if (showresource) { shownresource = entry; NearInfinity.getInstance().setViewable(ResourceFactory.getResource(entry)); } } else BrowserMenuBar.getInstance().resourceEntrySelected(null); } // --------------------- End Interface TreeSelectionListener --------------------- @Override public boolean requestFocusInWindow() { return tree.requestFocusInWindow(); } public ResourceEntry getSelected() { Object node = tree.getLastSelectedPathComponent(); if (node instanceof ResourceEntry) return (ResourceEntry)node; return null; } public void reloadRenderer() { tree.setCellRenderer(new ResourceTreeRenderer()); } public void select(ResourceEntry entry) { if (entry == null) tree.clearSelection(); else if (entry != shownresource) { TreePath tp = ResourceFactory.getResources().getPathToNode(entry); tree.scrollPathToVisible(tp); tree.addSelectionPath(tp); } } public ResourceTreeModel getModel() { return (ResourceTreeModel)tree.getModel(); } public void setModel(ResourceTreeModel treemodel) { nextstack.removeAllElements(); prevstack.removeAllElements(); bnext.setEnabled(false); bprev.setEnabled(false); tree.setModel(treemodel); tree.repaint(); } public void expandAll() { ResourceTreeModel model = (ResourceTreeModel)tree.getModel(); if (model != null) { ResourceTreeFolder root = (ResourceTreeFolder)model.getRoot(); processAllNodes(tree, new TreePath(root), true); } } public void collapseAll() { ResourceTreeModel model = (ResourceTreeModel)tree.getModel(); if (model != null) { ResourceTreeFolder root = (ResourceTreeFolder)model.getRoot(); processAllNodes(tree, new TreePath(root), false); tree.expandPath(new TreePath(root)); // virtual root node is always expanded } } public void expandSelected() { TreePath path = tree.getSelectionPath(); if (path != null && path.getPathCount() > 1) { Object node = path.getPathComponent(1); if (node instanceof ResourceTreeFolder) { Object root = path.getPathComponent(0); processAllNodes(tree, new TreePath(new Object[]{root, node}), true); } } } public void collapseSelected() { TreePath path = tree.getSelectionPath(); if (path != null && path.getPathCount() > 1) { Object node = path.getPathComponent(1); if (node instanceof ResourceTreeFolder) { Object root = path.getPathComponent(0); processAllNodes(tree, new TreePath(new Object[]{root, node}), false); } } } /** Attempts to rename the specified file resource entry. */ static void renameResource(FileResourceEntry entry) { String filename = JOptionPane.showInputDialog(NearInfinity.getInstance(), "Enter new filename", "Rename " + entry.toString(), JOptionPane.QUESTION_MESSAGE); if (filename == null) { return; } if (!filename.toUpperCase(Locale.ENGLISH).endsWith(entry.getExtension())) { filename = filename + '.' + entry.getExtension(); } if (Files.exists(entry.getActualPath().getParent().resolve(filename))) { JOptionPane.showMessageDialog(NearInfinity.getInstance(), "File already exists!", "Error", JOptionPane.ERROR_MESSAGE); return; } try { entry.renameFile(filename, false); } catch (IOException e) { JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error renaming file \"" + filename + "\"!", "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); return; } ResourceFactory.getResources().resourceEntryChanged(entry); } /** Attempts to delete the specified resource if it exists as a file in the game path. */ static void deleteResource(ResourceEntry entry) { if (entry instanceof FileResourceEntry) { String options[] = {"Delete", "Cancel"}; if (JOptionPane.showOptionDialog(NearInfinity.getInstance(), "Are you sure you want to delete " + entry + '?', "Delete file", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]) != 0) return; NearInfinity.getInstance().removeViewable(); ResourceFactory.getResources().removeResourceEntry(entry); Path bakFile = getBackupFile(entry); if (bakFile != null) { try { Files.delete(bakFile); } catch (IOException e) { e.printStackTrace(); } } try { ((FileResourceEntry)entry).deleteFile(); } catch (IOException e) { JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error deleting file \"" + entry.getResourceName() + "\"!", "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); } } else if (entry instanceof BIFFResourceEntry) { String options[] = {"Delete", "Cancel"}; if (JOptionPane.showOptionDialog(NearInfinity.getInstance(), "Are you sure you want to delete the " + "override file " + entry + '?', "Delete file", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]) != 0) return; NearInfinity.getInstance().removeViewable(); Path bakFile = getBackupFile(entry); if (bakFile != null) { try { Files.delete(bakFile); } catch (IOException e) { e.printStackTrace(); } } try { ((BIFFResourceEntry)entry).deleteOverride(); } catch (IOException e) { JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error deleting file \"" + entry.getResourceName() + "\"!", "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); } } } /** Attempts to restore the specified resource entry if it's backed up by an associated "*.bak" file. */ static void restoreResource(ResourceEntry entry) { if (entry != null) { final String[] options = { "Restore", "Cancel" }; final String msgBackup = "Are you sure you want to restore " + entry + " with a previous version?"; final String msgBiffed = "Are you sure you want to restore the biffed version of " + entry + "?"; Path bakFile = getBackupFile(entry); boolean isBackedUp = (bakFile != null) && (bakFile.getFileName().toString().toLowerCase(Locale.ENGLISH).endsWith(".bak")); if (JOptionPane.showOptionDialog(NearInfinity.getInstance(), isBackedUp ? msgBackup : msgBiffed, "Restore backup", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]) == JOptionPane.YES_OPTION) { NearInfinity.getInstance().removeViewable(); if (bakFile != null && (bakFile.getFileName().toString().toLowerCase(Locale.ENGLISH).endsWith(".bak"))) { // .bak available -> restore .bak version Path curFile = getCurrentFile(entry); Path tmpFile = getTempFile(curFile); if (curFile != null && Files.isRegularFile(curFile) && bakFile != null && Files.isRegularFile(bakFile)) { try { Files.move(curFile, tmpFile); try { Files.move(bakFile, curFile); try { Files.delete(tmpFile); } catch (IOException e) { e.printStackTrace(); } JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Backup has been restored successfully.", "Restore backup", JOptionPane.INFORMATION_MESSAGE); return; } catch (IOException e) { // Worst possible scenario: failed restore operation can't restore original resource String path = tmpFile.getParent().toString(); String tmpName = tmpFile.getFileName().toString(); String curName = curFile.getFileName().toString(); JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error while restoring resource.\n" + "Near Infinity is unable to recover from the restore operation.\n" + String.format("Please manually rename the file \"%1$s\" into \"%2$s\", located in \n + \"%3$s\"", tmpName, curName, path), "Critical Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); return; } } catch (IOException e) { e.printStackTrace(); } } JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error while restoring resource.\nRestore operation has been cancelled.", "Error", JOptionPane.ERROR_MESSAGE); } else if (entry instanceof BIFFResourceEntry && entry.hasOverride()) { // Biffed and no .bak available -> delete overridden copy try { ((BIFFResourceEntry)entry).deleteOverride(); JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Backup has been restored successfully.", "Restore backup", JOptionPane.INFORMATION_MESSAGE); } catch (IOException e) { JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error removing file \"" + entry + "\" from override folder!", "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); } } } } } private static void processAllNodes(JTree tree, TreePath parent, boolean expand) { if (tree != null && parent != null) { Object node = parent.getLastPathComponent(); if (node instanceof ResourceTreeFolder) { ResourceTreeFolder folder = (ResourceTreeFolder)node; if (folder.getChildCount() >= 0) { List<ResourceTreeFolder> list = folder.getFolders(); for (int i = 0, size = list.size(); i < size; i++) { ResourceTreeFolder f = list.get(i); TreePath path = parent.pathByAddingChild(f); processAllNodes(tree, path, expand); } } } if (expand) { tree.expandPath(parent); } else { tree.collapsePath(parent); } } } /** * Returns whether a backup exists in the same folder as the specified resource entry * or a biffed file has been overriden. */ static boolean isBackupAvailable(ResourceEntry entry) { if (entry != null) { return (getBackupFile(entry) != null || (entry instanceof BIFFResourceEntry && entry.hasOverride())); } return false; } // Returns the backup file of the specified resource entry if available or null. // A backup file is either a *.bak file or a biffed file which has been overridden. private static Path getBackupFile(ResourceEntry entry) { Path file = getCurrentFile(entry); if (entry instanceof FileResourceEntry || (entry instanceof BIFFResourceEntry && entry.hasOverride())) { if (file != null) { Path bakFile = file.getParent().resolve(file.getFileName().toString() + ".bak"); if (Files.isRegularFile(bakFile)) { return bakFile; } } } else if (entry instanceof BIFFResourceEntry) { return file; } return null; } // Returns the actual physical file of the given resource entry or null. private static Path getCurrentFile(ResourceEntry entry) { if (entry instanceof FileResourceEntry || (entry instanceof BIFFResourceEntry && entry.hasOverride())) { return entry.getActualPath(); } else { return null; } } // Returns an unoccupied filename based on 'file'. private static Path getTempFile(Path file) { Path retVal = null; if (file != null && Files.isRegularFile(file)) { final String fmt = ".%03d"; Path filePath = file.getParent(); String fileName = file.getFileName().toString(); for (int i = 0; i < 1000; i++) { Path tmp = filePath.resolve(fileName + String.format(fmt, i)); if (!Files.exists(tmp) && Files.notExists(tmp)) { retVal = tmp; break; } } } return retVal; } // -------------------------- INNER CLASSES -------------------------- private final class TreeKeyListener extends KeyAdapter implements ActionListener { private static final int TIMER_DELAY = 900; private String currentkey = ""; private final Timer timer; private TreeKeyListener() { timer = new Timer(TIMER_DELAY, this); timer.setRepeats(false); } @Override public void keyTyped(KeyEvent event) { currentkey += new Character(event.getKeyChar()).toString().toUpperCase(Locale.ENGLISH); if (timer.isRunning()) timer.restart(); else timer.start(); int startrow = 0; if (tree.getSelectionPath() != null) startrow = tree.getRowForPath(tree.getSelectionPath()); for (int i = startrow; i < tree.getRowCount(); i++) { TreePath path = tree.getPathForRow(i); if (path != null && path.getLastPathComponent() instanceof ResourceEntry && path.getLastPathComponent().toString().toUpperCase(Locale.ENGLISH).startsWith(currentkey)) { showresource = false; tree.scrollPathToVisible(path); tree.addSelectionPath(path); return; } } if (startrow > 0) { for (int i = 0; i < startrow; i++) { TreePath path = tree.getPathForRow(i); if (path != null && path.getLastPathComponent() instanceof ResourceEntry && path.getLastPathComponent().toString().toUpperCase(Locale.ENGLISH).startsWith(currentkey)) { showresource = false; tree.scrollPathToVisible(path); tree.addSelectionPath(path); return; } } } currentkey = ""; shownresource = null; tree.clearSelection(); } @Override public void actionPerformed(ActionEvent event) { currentkey = ""; if (tree.getLastSelectedPathComponent() != null && tree.getLastSelectedPathComponent() instanceof ResourceEntry) { shownresource = (ResourceEntry)tree.getLastSelectedPathComponent(); NearInfinity.getInstance().setViewable(ResourceFactory.getResource(shownresource)); } showresource = true; } } private final class TreeMouseListener extends MouseAdapter { private final TreePopupMenu pmenu = new TreePopupMenu(); @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { TreePath path = tree.getClosestPathForLocation(e.getX(), e.getY()); if (path != null && path.getPathCount() > 2) { showresource = false; tree.addSelectionPath(path); pmenu.show(e.getComponent(), e.getX(), e.getY()); } } else showresource = true; } } private final class TreePopupMenu extends JPopupMenu implements ActionListener { private final JMenuItem mi_open = new JMenuItem("Open"); private final JMenuItem mi_opennew = new JMenuItem("Open in new window"); private final JMenuItem mi_export = new JMenuItem("Export"); private final JMenuItem mi_addcopy = new JMenuItem("Add copy of"); private final JMenuItem mi_rename = new JMenuItem("Rename"); private final JMenuItem mi_delete = new JMenuItem("Delete"); private final JMenuItem mi_restore = new JMenuItem("Restore backup"); TreePopupMenu() { add(mi_open); add(mi_opennew); add(mi_export); add(mi_addcopy); add(mi_rename); add(mi_delete); add(mi_restore); mi_open.addActionListener(this); mi_opennew.addActionListener(this); mi_export.addActionListener(this); mi_addcopy.addActionListener(this); mi_rename.addActionListener(this); mi_delete.addActionListener(this); mi_restore.addActionListener(this); mi_opennew.setFont(mi_opennew.getFont().deriveFont(Font.PLAIN)); mi_export.setFont(mi_opennew.getFont()); mi_addcopy.setFont(mi_opennew.getFont()); mi_rename.setFont(mi_opennew.getFont()); mi_delete.setFont(mi_opennew.getFont()); mi_restore.setFont(mi_opennew.getFont()); } @Override public void show(Component invoker, int x, int y) { super.show(invoker, x, y); mi_rename.setEnabled(tree.getLastSelectedPathComponent() instanceof FileResourceEntry); if (tree.getLastSelectedPathComponent() instanceof ResourceEntry) { ResourceEntry entry = (ResourceEntry)tree.getLastSelectedPathComponent(); mi_delete.setEnabled(entry != null && entry.hasOverride() || entry instanceof FileResourceEntry); mi_restore.setEnabled(isBackupAvailable(entry)); } else { mi_delete.setEnabled(false); mi_restore.setEnabled(false); } } @Override public void actionPerformed(ActionEvent event) { showresource = true; ResourceEntry node = (ResourceEntry)tree.getLastSelectedPathComponent(); if (event.getSource() == mi_open) { if (prevnextnode != null) prevstack.push(prevnextnode); nextstack.removeAllElements(); bnext.setEnabled(false); bprev.setEnabled(prevnextnode != null); prevnextnode = node; shownresource = node; NearInfinity.getInstance().setViewable(ResourceFactory.getResource(node)); } else if (event.getSource() == mi_opennew) { Resource res = ResourceFactory.getResource(node); if (res != null) new ViewFrame(NearInfinity.getInstance(), res); } else if (event.getSource() == mi_export) { ResourceFactory.exportResource(node, NearInfinity.getInstance()); } else if (event.getSource() == mi_addcopy) { ResourceFactory.saveCopyOfResource(node); } else if (event.getSource() == mi_rename) { if (tree.getLastSelectedPathComponent() instanceof FileResourceEntry) { renameResource((FileResourceEntry)tree.getLastSelectedPathComponent()); } } else if (event.getSource() == mi_delete) { if (tree.getLastSelectedPathComponent() instanceof ResourceEntry) { deleteResource((ResourceEntry)tree.getLastSelectedPathComponent()); } } else if (event.getSource() == mi_restore) { if (tree.getLastSelectedPathComponent() instanceof ResourceEntry) { restoreResource((ResourceEntry)tree.getLastSelectedPathComponent()); } } } } private static final class ResourceTreeRenderer extends DefaultTreeCellRenderer { private ResourceTreeRenderer() { } @Override public Component getTreeCellRendererComponent(JTree tree, Object o, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (leaf && o instanceof ResourceEntry) { super.getTreeCellRendererComponent(tree, o, sel, expanded, leaf, row, hasFocus); setIcon(((ResourceEntry)o).getIcon()); return this; } else return super.getTreeCellRendererComponent(tree, o, sel, expanded, leaf, row, hasFocus); } } }