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;