diff --git a/ugs-core/src/com/willwinder/universalgcodesender/utils/Settings.java b/ugs-core/src/com/willwinder/universalgcodesender/utils/Settings.java index d0c4c41c4..1c5710fe2 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/utils/Settings.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/utils/Settings.java @@ -1,5 +1,5 @@ /* - Copyright 2014-2023 Will Winder + Copyright 2014-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -29,8 +29,20 @@ This file is part of Universal Gcode Sender (UGS). import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; import java.util.logging.Logger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.List; + + public class Settings { private static final Logger logger = Logger.getLogger(Settings.class.getName()); @@ -120,6 +132,11 @@ public class Settings { */ private boolean showTranslationsWarning = true; + /** + * The last working directory used by the file browser + */ + private String lastWorkingDirectory = System.getProperty("user.home"); + /** * The GSON deserialization doesn't do anything beyond initialize what's in the json document. Call finalizeInitialization() before using the Settings. */ @@ -556,6 +573,14 @@ public void setShowTranslationsWarning(boolean showTranslationsWarning) { this.showTranslationsWarning = showTranslationsWarning; } + public String getLastWorkingDirectory() { + return lastWorkingDirectory; + } + + public void setLastWorkingDirectory(String lastWorkingDirectory) { + this.lastWorkingDirectory = lastWorkingDirectory; + } + public static class FileStats { public Position minCoordinate; public Position maxCoordinate; diff --git a/ugs-core/src/resources/MessagesBundle_en_US.properties b/ugs-core/src/resources/MessagesBundle_en_US.properties index e56f27482..75924e014 100644 --- a/ugs-core/src/resources/MessagesBundle_en_US.properties +++ b/ugs-core/src/resources/MessagesBundle_en_US.properties @@ -315,6 +315,8 @@ platform.window.serialconsole = Console platform.window.serialconsole.tooltip = Console displaying messages to and from the controller. platform.window.visualizer = Visualizer platform.window.visualizer.tooltip = 3D view of the current operation. +platform.window.fileBrowser = File Browser +platform.window.fileBrowser.tooltip = Load a folder to view and work with files. platform.visualizer.edit.options.title = Visualizer options platform.visualizer.color.background = Background Color platform.visualizer.tool = Show tool location diff --git a/ugs-platform/ugs-platform-plugin-dro/src/main/java/com/willwinder/ugs/nbp/dro/MachineStatusTopComponent.java b/ugs-platform/ugs-platform-plugin-dro/src/main/java/com/willwinder/ugs/nbp/dro/MachineStatusTopComponent.java index d3290be12..607c37378 100644 --- a/ugs-platform/ugs-platform-plugin-dro/src/main/java/com/willwinder/ugs/nbp/dro/MachineStatusTopComponent.java +++ b/ugs-platform/ugs-platform-plugin-dro/src/main/java/com/willwinder/ugs/nbp/dro/MachineStatusTopComponent.java @@ -43,7 +43,8 @@ This file is part of Universal Gcode Sender (UGS). ) @TopComponent.Registration( mode = Mode.LEFT_TOP, - openAtStartup = true + openAtStartup = true, + position = 100 ) @ActionID( category = LocalizingService.LocationStatusCategory, diff --git a/ugs-platform/ugs-platform-ugscore/src/main/java/com/willwinder/ugs/nbp/core/control/FileBrowserTopComponent.java b/ugs-platform/ugs-platform-ugscore/src/main/java/com/willwinder/ugs/nbp/core/control/FileBrowserTopComponent.java new file mode 100644 index 000000000..1abda52d4 --- /dev/null +++ b/ugs-platform/ugs-platform-ugscore/src/main/java/com/willwinder/ugs/nbp/core/control/FileBrowserTopComponent.java @@ -0,0 +1,88 @@ +/* + Copyright 2016-2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UGS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UGS. If not, see . + */ +package com.willwinder.ugs.nbp.core.control; + +import com.willwinder.ugs.nbp.lib.Mode; +import com.willwinder.ugs.nbp.lib.lookup.CentralLookup; +import com.willwinder.ugs.nbp.lib.services.TopComponentLocalizer; +import com.willwinder.universalgcodesender.model.BackendAPI; +import com.willwinder.ugs.nbp.core.panels.FileBrowserPanel; +import static com.willwinder.ugs.nbp.lib.services.LocalizingService.FileBrowserPanelCategory; +import static com.willwinder.ugs.nbp.lib.services.LocalizingService.FileBrowserPanelActionId; +import static com.willwinder.ugs.nbp.lib.services.LocalizingService.FileBrowserPanelWindowPath; +import static com.willwinder.ugs.nbp.lib.services.LocalizingService.FileBrowserPanelTitle; +import static com.willwinder.ugs.nbp.lib.services.LocalizingService.FileBrowserPanelTooltip; + + +import java.awt.BorderLayout; + +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.modules.OnStart; +import org.openide.windows.TopComponent; + +/** + * Top component which displays something. + */ +@TopComponent.Description( + preferredID = "FileBrowserTopComponent" +) +@TopComponent.Registration(mode = Mode.LEFT_TOP, openAtStartup = false, position = 2200) +@ActionID(category = FileBrowserPanelCategory, id = FileBrowserPanelActionId) +@ActionReference(path = FileBrowserPanelWindowPath) +@TopComponent.OpenActionRegistration( + displayName = "", + preferredID = "FileBrowserTopComponent" +) +public final class FileBrowserTopComponent extends TopComponent { + + public FileBrowserTopComponent() { + this.setLayout(new BorderLayout()); + BackendAPI backend = CentralLookup.getDefault().lookup(BackendAPI.class); + FileBrowserPanel panel = new FileBrowserPanel(backend); + this.add(panel, BorderLayout.CENTER); + } + + @Override + public void componentOpened() { + setName(FileBrowserPanelTitle); + setToolTipText(FileBrowserPanelTooltip); + } + + @Override + public void componentClosed() { + // TODO add custom code on component closing + } + + public void writeProperties(java.util.Properties p) { + // better to version settings since initial version as advocated at + // http://wiki.apidesign.org/wiki/PropertyFiles + p.setProperty("version", "1.0"); + } + + public void readProperties(java.util.Properties p) { + } + + @OnStart + public static class Localizer extends TopComponentLocalizer { + public Localizer() { + super(FileBrowserPanelCategory, FileBrowserPanelActionId, FileBrowserPanelTitle); + } + } +} diff --git a/ugs-platform/ugs-platform-ugscore/src/main/java/com/willwinder/ugs/nbp/core/panels/FileBrowserPanel.java b/ugs-platform/ugs-platform-ugscore/src/main/java/com/willwinder/ugs/nbp/core/panels/FileBrowserPanel.java new file mode 100644 index 000000000..651c67fef --- /dev/null +++ b/ugs-platform/ugs-platform-ugscore/src/main/java/com/willwinder/ugs/nbp/core/panels/FileBrowserPanel.java @@ -0,0 +1,334 @@ +/* + Copyright 2016-2024 Will Winder + + This file is part of Universal Gcode Sender (UGS). + + UGS is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + UGS is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with UGS. If not, see . + */ +package com.willwinder.ugs.nbp.core.panels; + +import com.willwinder.ugs.nbp.core.actions.OpenFileAction; +import com.willwinder.universalgcodesender.listeners.ControllerState; +import com.willwinder.universalgcodesender.listeners.UGSEventListener; +import com.willwinder.universalgcodesender.model.BackendAPI; +import com.willwinder.universalgcodesender.model.UGSEvent; +import com.willwinder.universalgcodesender.model.events.ControllerStateEvent; +import com.willwinder.universalgcodesender.model.events.ControllerStatusEvent; + +import java.awt.Component; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.BorderLayout; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import java.io.File; + +import org.openide.util.ImageUtilities; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.JTree; +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + + +/** + * Displays a list of files and directories in a given directory. + * + * @author andrewmurraydavid + */ +public final class FileBrowserPanel extends JPanel implements UGSEventListener { + private final transient BackendAPI backend; + private final JTree fileTree; + private File currentFile; + private final JTextField currentPathField; + private final JCheckBox showHiddenCheckBox; + private final JButton goButton; + + public FileBrowserPanel(BackendAPI backend) { + this.backend = backend; + backend.addUGSEventListener(this); + setLayout(new BorderLayout()); + + JPanel northPanel = new JPanel(new BorderLayout()); + currentPathField = new JTextField(); + northPanel.add(currentPathField, BorderLayout.CENTER); + + goButton = new JButton("Go"); + northPanel.add(goButton, BorderLayout.EAST); + + showHiddenCheckBox = new JCheckBox("Show Hidden", false); + northPanel.add(showHiddenCheckBox, BorderLayout.SOUTH); + + add(northPanel, BorderLayout.NORTH); + + File initialDirectory = new File(backend.getSettings().getLastWorkingDirectory()); + + DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(new FileNode(initialDirectory)); + DefaultTreeModel treeModel = new DefaultTreeModel(rootNode); + fileTree = new JTree(treeModel); + fileTree.setCellRenderer(new FileTreeCellRenderer()); + fileTree.setRootVisible(false); + fileTree.setShowsRootHandles(true); + JScrollPane treeScroll = new JScrollPane(fileTree); + add(treeScroll, BorderLayout.CENTER); + + setDirectory(initialDirectory); + + fileTree.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + TreePath path = fileTree.getPathForLocation(e.getX(), e.getY()); + openFileFromFileNode(path); + } + } + }); + + fileTree.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + TreePath path = fileTree.getSelectionPath(); // Get the selected path + if (path != null) { + openFileFromFileNode(path); + } + } else if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE) { + // Navigate up when backspace is pressed + File parentFile = currentFile.getParentFile(); + if (parentFile != null) { + setDirectory(parentFile); + } + } + } + }); + + fileTree.addTreeWillExpandListener(new TreeWillExpandListener() { + @Override + public void treeWillExpand(TreeExpansionEvent event) { + TreePath path = event.getPath(); + DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); + FileNode fileNode = (FileNode) node.getUserObject(); + + if (fileNode.isDirectory()) { + node.removeAllChildren(); + createChildren(node, fileNode.getFile(), showHiddenCheckBox.isSelected()); + ((DefaultTreeModel) fileTree.getModel()).nodeStructureChanged(node); + } + } + + @Override + public void treeWillCollapse(TreeExpansionEvent event) { + // No need to handle collapse events in this context + } + }); + + ActionListener goActionListener = e -> changeDirectory(currentPathField.getText()); + goButton.addActionListener(goActionListener); + currentPathField.addActionListener(goActionListener); + + showHiddenCheckBox.addItemListener(e -> { + boolean showHidden = e.getStateChange() == ItemEvent.SELECTED; + refreshFileList(showHidden); + }); + + addAncestorListener(new AncestorListener() { + @Override + public void ancestorAdded(AncestorEvent event) { + // Request focus for the JTree when the panel is shown + fileTree.requestFocusInWindow(); + } + + @Override + public void ancestorRemoved(AncestorEvent event) { + } + + @Override + public void ancestorMoved(AncestorEvent event) { + // Handle case when the panel or its ancestor is moved, if necessary + fileTree.requestFocusInWindow(); + } + }); + } + + private void openFileFromFileNode(TreePath path) { + if (!this.isEnabled()) { + return; + } + + if (path != null) { + DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent(); + FileNode fileNode = (FileNode) selectedNode.getUserObject(); + if (fileNode.displayName.startsWith("..")) { + DefaultMutableTreeNode upperNode = (DefaultMutableTreeNode) selectedNode.getParent(); + File upperPath = ((FileNode) upperNode.getUserObject()).getFile(); + File parentFile = upperPath.getParentFile(); + setDirectory(parentFile); + } else if (fileNode.getFile().isDirectory()) { + setDirectory(fileNode.getFile()); + } else if (!fileNode.displayName.startsWith(".")) { + File gcodeFile = fileNode.getFile(); + new OpenFileAction(gcodeFile).actionPerformed(null); + } + } + } + + public void setDirectory(File directory) { + currentFile = directory; + currentPathField.setText(directory.getAbsolutePath()); + refreshFileList(showHiddenCheckBox.isSelected()); + backend.getSettings().setLastWorkingDirectory(directory.getAbsolutePath()); + } + + private void refreshFileList(boolean showHidden) { + DefaultMutableTreeNode newRootNode = new DefaultMutableTreeNode(new FileNode(currentFile)); + if (currentFile.getParentFile() != null) { + newRootNode.add(new DefaultMutableTreeNode(new FileNode(null, ". (" + currentFile.getName() + ")"))); + newRootNode.add(new DefaultMutableTreeNode(new FileNode(null, ".. (" + currentFile.getParentFile().getName() + ")"))); + } + createChildren(newRootNode, currentFile, showHidden); + + DefaultTreeModel model = (DefaultTreeModel) fileTree.getModel(); + model.setRoot(newRootNode); + model.reload(); + + currentPathField.setText(currentFile.getAbsolutePath()); + } + + + private void changeDirectory(String path) { + File newDirectory = new File(path); + if (newDirectory.exists() && newDirectory.isDirectory()) { + setDirectory(newDirectory); + } else { + JOptionPane.showMessageDialog(this, "Directory does not exist: " + path, "Error", JOptionPane.ERROR_MESSAGE); + } + } + + private void createChildren(DefaultMutableTreeNode node, File file, boolean showHidden) { + if (file == null) { + return; + } + + File[] files = file.listFiles(); + if (files != null) { + for (File child : files) { + if (!showHidden && (child.isHidden() || child.getName().startsWith(".") || child.getName().startsWith("$"))) { + continue; + } + DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(new FileNode(child)); + node.add(childNode); + if (child.isDirectory()) { + childNode.add(new DefaultMutableTreeNode(new FileNode(null, "Loading..."))); + } + } + } + } + + private void setPanelEnabled(boolean enabled) { + this.setEnabled(enabled); + goButton.setEnabled(enabled); + fileTree.setEnabled(enabled); + currentPathField.setEnabled(enabled); + showHiddenCheckBox.setEnabled(enabled); + } + + private void setEnabledFromStatus(ControllerState controllerStateEvent) { + switch (controllerStateEvent) { + case DISCONNECTED, IDLE, UNKNOWN: + setPanelEnabled(true); + break; + default: + setPanelEnabled(false); + break; + } + } + + @Override + public void UGSEvent(UGSEvent evt) { + if (evt instanceof ControllerStateEvent controllerStateEvent) { + setEnabledFromStatus(controllerStateEvent.getState()); + } else if (evt instanceof ControllerStatusEvent controllerStatusEvent) { + setEnabledFromStatus(controllerStatusEvent.getStatus().getState()); + } + } + + private static class FileNode { + private final File file; + private final String displayName; + + public FileNode(File file) { + this.file = file; + this.displayName = file.getName().isEmpty() ? file.getPath() : file.getName(); + } + + public FileNode(File file, String displayName) { + this.file = file; + this.displayName = displayName; + } + + public boolean isDirectory() { + return file.isDirectory(); + } + + public File getFile() { + return file; + } + + @Override + public String toString() { + return displayName; + } + } + + public static class FileTreeCellRenderer extends DefaultTreeCellRenderer { + public static final String SMALL_GCODE_ICON = "icons/new.svg"; + + @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); + + DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; + if (!(node.getUserObject() instanceof FileNode fileNode)) { + return this; + } + + if (fileNode.getFile() == null) { + setIcon(getDefaultOpenIcon()); + setDisabledIcon(getDefaultOpenIcon()); + } else if (fileNode.getFile().isFile()) { + String fileName = fileNode.getFile().getName(); + if (fileName.matches(".*\\.(gcode|GCODE|cnc|CNC|nc|NC|ngc|NGC|tap|TAP|txt|TXT|gc|GC)")) { + setIcon(ImageUtilities.loadImageIcon(SMALL_GCODE_ICON, false)); + setDisabledIcon(ImageUtilities.loadImageIcon(SMALL_GCODE_ICON, false)); + } + } + + return this; + } + } +} diff --git a/ugs-platform/ugs-platform-ugslib/src/main/java/com/willwinder/ugs/nbp/lib/services/LocalizingService.java b/ugs-platform/ugs-platform-ugslib/src/main/java/com/willwinder/ugs/nbp/lib/services/LocalizingService.java index 34927fef8..c5d991cbd 100644 --- a/ugs-platform/ugs-platform-ugslib/src/main/java/com/willwinder/ugs/nbp/lib/services/LocalizingService.java +++ b/ugs-platform/ugs-platform-ugslib/src/main/java/com/willwinder/ugs/nbp/lib/services/LocalizingService.java @@ -1,5 +1,5 @@ /* - Copyright 2016-2021 Will Winder + Copyright 2016-2024 Will Winder This file is part of Universal Gcode Sender (UGS). @@ -298,6 +298,12 @@ public class LocalizingService { public final static String DiagnosticsActionId = "com.willwinder.ugs.nbp.core.windows.DiagnosticsTopComponent"; public final static String DiagnosticsCategory = CATEGORY_WINDOW; + public final static String FileBrowserPanelTitle = Localization.getString("platform.window.fileBrowser", lang); + public final static String FileBrowserPanelTooltip = Localization.getString("platform.window.fileBrowser.tooltip", lang); + public final static String FileBrowserPanelActionId = "com.willwinder.ugs.nbp.core.windows.FileBrowserTopComponent"; + public final static String FileBrowserPanelWindowPath = MENU_WINDOW; + public final static String FileBrowserPanelCategory = CATEGORY_WINDOW; + public final static String RunFromTitleKey = "platform.menu.runFrom"; public final static String RunFromTitle = Localization.getString(RunFromTitleKey, lang); public final static String RunFromWindowPath = MENU_PROGRAM;