entries = channelSftp.ls(path);
+ // remove hidden directories (TODO create a setting for that)
+ entries.removeIf(entry -> entry.getFilename().equals(".") || entry.getFilename().equals("..") || entry.getFilename().startsWith("."));
+ return entries;
+ }
+ catch(SftpException sftpex) {
+ throw sftpex;
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (channelSftp != null)
+ channelSftp.disconnect();
+ }
+ }
+
+ /*
+ * ========== END File System operations ==========
+ */
+
+
+ /*
+ * ========== BEGIN Other ==========
+ */
+
+ public static String executeCommand(String command) {
+ System.out.println("> " + command);
+ Channel channel = null;
+ InputStream in = null;
+ try {
+ checkValidityOrCreateSession();
+ channel = session.openChannel("exec");
+ ((ChannelExec) channel).setCommand(command);
+ channel.connect();
+ in = channel.getInputStream();
+ String returnText = new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ in.close();
+ // Remove possible \r\n at the end of the string
+ returnText = returnText.replaceAll("[\r\n]+$", "").trim();
+ System.out.println("< " + returnText);
+ return returnText;
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (JSchException e) {
+ e.printStackTrace();
+ } finally {
+ if (channel != null) {
+ channel.disconnect();
+ }
+ }
+ return null;
+ }
+
+ /*
+ * ========== END Other ==========
+ */
+
+
+}
+/*
+ * When executing a command on a remote server using SSH with Jsch in Java, we
+ * typically want to capture the output of the command as it is executed on the
+ * remote server. Therefore, we need to obtain an input stream from the remote
+ * server that will allow us to read the output of the command.
+ *
+ * The channel.getInputStream() method returns an input stream that is connected
+ * to the standard output of the remote command being executed. This means that
+ * any output produced by the command will be sent to the input stream, which we
+ * can then read in our Java program to obtain the output.
+ *
+ * On the other hand, the channel.getOutputStream() method returns an output
+ * stream that is connected to the standard input of the remote command being
+ * executed. This means that any input provided to the output stream will be
+ * sent to the remote command as its standard input. While this may be useful in
+ * some cases, it is not typically what we want when executing a command on a
+ * remote server and capturing its output.
+ *
+ * So to sum up, we use channel.getInputStream() to obtain an input stream that
+ * we can use to read the output of the command being executed on the remote
+ * server.
+ */
\ No newline at end of file
diff --git a/Guify/src/code/TransferProgress.java b/Guify/src/code/TransferProgress.java
new file mode 100644
index 0000000..d8bbe63
--- /dev/null
+++ b/Guify/src/code/TransferProgress.java
@@ -0,0 +1,59 @@
+package code;
+
+/**
+ *
+ * An object representing the transfer progress of a
+ * file between the server and the host machine.
+ *
+ */
+public class TransferProgress {
+
+ // Transfer statuses
+ public static final int INIT = 0;
+ public static final int UPDATING = 1;
+ public static final int END = 2;
+
+ private String source;
+ private String destination;
+ private long totalBytes;
+ private long transferredBytes;
+ private int operation;
+ private int transferStatus;
+
+ public String getSource() {
+ return source;
+ }
+ public void setSource(String source) {
+ this.source = source;
+ }
+ public String getDestination() {
+ return destination;
+ }
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+ public long getTotalBytes() {
+ return totalBytes;
+ }
+ public void setTotalBytes(long totalBytes) {
+ this.totalBytes = totalBytes;
+ }
+ public long getTransferredBytes() {
+ return transferredBytes;
+ }
+ public void setTransferredBytes(long transferredBytes) {
+ this.transferredBytes = transferredBytes;
+ }
+ public int getOperation() {
+ return operation;
+ }
+ public void setOperation(int operation) {
+ this.operation = operation;
+ }
+ public int getTransferStatus() {
+ return transferStatus;
+ }
+ public void setTransferStatus(int transferStatus) {
+ this.transferStatus = transferStatus;
+ }
+}
diff --git a/Guify/src/code/TreeNode.java b/Guify/src/code/TreeNode.java
new file mode 100644
index 0000000..bf98b99
--- /dev/null
+++ b/Guify/src/code/TreeNode.java
@@ -0,0 +1,14 @@
+package code;
+
+/**
+ *
+ * A class representing a
+ * "tree -J" result
+ *
+ */
+public class TreeNode {
+ public String type;
+ public String name;
+ public TreeNode[] contents;
+ public String error;
+}
diff --git a/Guify/src/code/WrapLayout.java b/Guify/src/code/WrapLayout.java
new file mode 100644
index 0000000..81e366c
--- /dev/null
+++ b/Guify/src/code/WrapLayout.java
@@ -0,0 +1,195 @@
+package code;
+
+import java.awt.*;
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+/**
+ * FlowLayout subclass that fully supports wrapping of components.
+ */
+public class WrapLayout extends FlowLayout
+{
+ /**
+ *
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new WrapLayout
with a left
+ * alignment and a default 5-unit horizontal and vertical gap.
+ */
+ public WrapLayout()
+ {
+ super();
+ }
+
+ /**
+ * Constructs a new FlowLayout
with the specified
+ * alignment and a default 5-unit horizontal and vertical gap.
+ * The value of the alignment argument must be one of
+ * WrapLayout
, WrapLayout
,
+ * or WrapLayout
.
+ * @param align the alignment value
+ */
+ public WrapLayout(int align)
+ {
+ super(align);
+ }
+
+ /**
+ * Creates a new flow layout manager with the indicated alignment
+ * and the indicated horizontal and vertical gaps.
+ *
+ * The value of the alignment argument must be one of
+ * WrapLayout
, WrapLayout
,
+ * or WrapLayout
.
+ * @param align the alignment value
+ * @param hgap the horizontal gap between components
+ * @param vgap the vertical gap between components
+ */
+ public WrapLayout(int align, int hgap, int vgap)
+ {
+ super(align, hgap, vgap);
+ }
+
+ /**
+ * Returns the preferred dimensions for this layout given the
+ * visible components in the specified target container.
+ * @param target the component which needs to be laid out
+ * @return the preferred dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension preferredLayoutSize(Container target)
+ {
+ return layoutSize(target, true);
+ }
+
+ /**
+ * Returns the minimum dimensions needed to layout the visible
+ * components contained in the specified target container.
+ * @param target the component which needs to be laid out
+ * @return the minimum dimensions to lay out the
+ * subcomponents of the specified container
+ */
+ @Override
+ public Dimension minimumLayoutSize(Container target)
+ {
+ Dimension minimum = layoutSize(target, false);
+ minimum.width -= (getHgap() + 1);
+ return minimum;
+ }
+
+ /**
+ * Returns the minimum or preferred dimension needed to layout the target
+ * container.
+ *
+ * @param target target to get layout size for
+ * @param preferred should preferred size be calculated
+ * @return the dimension to layout the target container
+ */
+ private Dimension layoutSize(Container target, boolean preferred)
+ {
+ synchronized (target.getTreeLock())
+ {
+ // Each row must fit with the width allocated to the containter.
+ // When the container width = 0, the preferred width of the container
+ // has not yet been calculated so lets ask for the maximum.
+
+ int targetWidth = target.getSize().width;
+ Container container = target;
+
+ while (container.getSize().width == 0 && container.getParent() != null)
+ {
+ container = container.getParent();
+ }
+
+ targetWidth = container.getSize().width;
+
+ if (targetWidth == 0)
+ targetWidth = Integer.MAX_VALUE;
+
+ int hgap = getHgap();
+ int vgap = getVgap();
+ Insets insets = target.getInsets();
+ int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2);
+ int maxWidth = targetWidth - horizontalInsetsAndGap;
+
+ // Fit components into the allowed width
+
+ Dimension dim = new Dimension(0, 0);
+ int rowWidth = 0;
+ int rowHeight = 0;
+
+ int nmembers = target.getComponentCount();
+
+ for (int i = 0; i < nmembers; i++)
+ {
+ Component m = target.getComponent(i);
+
+ if (m.isVisible())
+ {
+ Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize();
+
+ // Can't add the component to current row. Start a new row.
+
+ if (rowWidth + d.width > maxWidth)
+ {
+ addRow(dim, rowWidth, rowHeight);
+ rowWidth = 0;
+ rowHeight = 0;
+ }
+
+ // Add a horizontal gap for all components after the first
+
+ if (rowWidth != 0)
+ {
+ rowWidth += hgap;
+ }
+
+ rowWidth += d.width;
+ rowHeight = Math.max(rowHeight, d.height);
+ }
+ }
+
+ addRow(dim, rowWidth, rowHeight);
+
+ dim.width += horizontalInsetsAndGap;
+ dim.height += insets.top + insets.bottom + vgap * 2;
+
+ // When using a scroll pane or the DecoratedLookAndFeel we need to
+ // make sure the preferred size is less than the size of the
+ // target containter so shrinking the container size works
+ // correctly. Removing the horizontal gap is an easy way to do this.
+
+ Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target);
+
+ if (scrollPane != null && target.isValid())
+ {
+ dim.width -= (hgap + 1);
+ }
+
+ return dim;
+ }
+ }
+
+ /*
+ * A new row has been completed. Use the dimensions of this row
+ * to update the preferred size for the container.
+ *
+ * @param dim update the width and height when appropriate
+ * @param rowWidth the width of the row to add
+ * @param rowHeight the height of the row to add
+ */
+ private void addRow(Dimension dim, int rowWidth, int rowHeight)
+ {
+ dim.width = Math.max(dim.width, rowWidth);
+
+ if (dim.height > 0)
+ {
+ dim.height += getVgap();
+ }
+
+ dim.height += rowHeight;
+ }
+}
diff --git a/Guify/src/controllers/DesktopController.java b/Guify/src/controllers/DesktopController.java
new file mode 100644
index 0000000..71dce70
--- /dev/null
+++ b/Guify/src/controllers/DesktopController.java
@@ -0,0 +1,437 @@
+package controllers;
+
+import com.google.gson.Gson;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.SftpException;
+
+import code.Constants;
+import code.Constants.Constants_FSOperations;
+import code.GuiAbstractions.Implementations.JFrameFactory;
+import views.interfaces.IDesktopFrame;
+import code.TreeNode;
+import code.Helper;
+import code.IDirectoryNodeButton;
+import code.SshEngine;
+import java.io.File;
+import java.util.*;
+import java.util.List;
+public class DesktopController {
+
+ /*
+ * ========== BEGIN Attributes ==========
+ */
+
+ private IDesktopFrame frame;
+ private String currentWorkingDirectory = "~";
+ private String lastSafeDirectory = null;
+ private List selectedNodes = new ArrayList();
+ public CutCopyPasteController cutCopyPasteController = new CutCopyPasteController();
+
+ /*
+ * ========== END Attributes ==========
+ */
+
+ /*
+ * ========== BEGIN Constructors ==========
+ */
+
+ public DesktopController() {
+ try {
+ frame = (IDesktopFrame) JFrameFactory.createJFrame(JFrameFactory.DESKTOP, this);
+ frame.drawComponentsForDirectory("~");
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /*
+ * ========== END Constructors ==========
+ */
+
+
+ /*
+ * ========== BEGIN Getters and Setters ==========
+ */
+
+ public String getCurrentWorkingDirectory() {
+ return currentWorkingDirectory;
+ }
+
+ public void setCurrentWorkingDirectory(String directory) {
+ if(directory.equals("~")) {
+ currentWorkingDirectory = SshEngine.executeCommand("pwd");
+ }
+ else {
+ currentWorkingDirectory = directory.trim();
+ }
+ }
+
+ public String getLastSafeDirectory() {
+ return lastSafeDirectory;
+ }
+
+ public void setLastSafeDirectory(String directory) {
+
+ if(directory == null) {
+ lastSafeDirectory = null;
+ return;
+ }
+
+ if(directory.equals("~")) {
+ lastSafeDirectory = SshEngine.executeCommand("pwd");
+ }
+ else {
+ lastSafeDirectory = directory.trim();
+ }
+ }
+
+ /*
+ * ========== END Getters and Setters ==========
+ */
+
+
+ /*
+ * ========== BEGIN Create desktop helper methods ==========
+ */
+
+ public TreeNode getTree() {
+ final int maxDepth = 3;
+ StringBuilder command = new StringBuilder("tree -Ji -L ");
+ command.append(maxDepth);
+ command.append(' ');
+ command.append(currentWorkingDirectory);
+ String jsonTree = SshEngine.executeCommand(command.toString());
+ TreeNode[] tree = null;
+
+ try {
+ // Might throw Invalid JSON exception because of incorrect
+ // JSON output returned from tree:
+ // https://gitlab.com/OldManProgrammer/unix-tree/-/issues/11
+ // Fixed in tree 2.1.1 https://gitlab.com/OldManProgrammer/unix-tree/-/commit/84fa3ddff51b30835a0f9c4a9e4c9225970f9aff
+ //
+ // For this reason, we temporarily explicitly avoid it to happen
+ jsonTree = jsonTree.replace("}{\"error\"", "},{\"error\"");
+ tree = new Gson().fromJson(jsonTree, TreeNode[].class);
+ return tree[0];
+ }
+ catch(Exception ex) {
+ return null;
+ }
+ }
+
+ public Vector getDirectoryElements() throws SftpException {
+ return SshEngine.ls(currentWorkingDirectory);
+ }
+
+ /*
+ * ========== END Create desktop helper methods ==========
+ */
+
+
+ /*
+ * ========== BEGIN Download and Upload section ==========
+ */
+
+ /**
+ * Downloads a file from the remote server to the local machine
+ * @param sourcePath Remote file's full path
+ * @param destinationPath Local file's full path
+ */
+ public void downloadFile(String sourcePath, String destinationPath) {
+ SshEngine.downloadFile(sourcePath, destinationPath);
+ }
+
+ /**
+ * Uploads files and folders to the remote server
+ * @param selectedNodes
+ * @throws SftpException
+ */
+ public void uploadToRemoteServer(File[] selectedNodes) throws SftpException {
+ if (selectedNodes.length > 0) {
+ List selectedFiles = new ArrayList();
+ List selectedDirectories = new ArrayList();
+ for (java.io.File file : selectedNodes) {
+ if(file.isFile()) {
+ selectedFiles.add(file);
+ }
+ else if(file.isDirectory()) {
+ selectedDirectories.add(file);
+ }
+ }
+
+ for(File file : selectedFiles) {
+ SshEngine.uploadFile(file, this.getCurrentWorkingDirectory());
+ }
+
+
+ if(selectedDirectories.size() > 0) {
+ for(File directory : selectedDirectories) {
+ SshEngine.uploadDirectoriesRecursively(directory, this.getCurrentWorkingDirectory());
+ }
+ }
+ }
+ }
+
+ /*
+ * ========== END Download and Upload section ==========
+ */
+
+
+ /*
+ * ========== BEGIN Selected Nodes section ==========
+ */
+
+ public void addSelectedNode(IDirectoryNodeButton node) {
+ selectedNodes.add(node);
+ node.setSelected(true);
+ }
+
+ public void removeSelectedNode(IDirectoryNodeButton node) {
+ selectedNodes.remove(node);
+ node.setSelected(false);
+ }
+
+ public void clearSelectedNodes() {
+ if(selectedNodes != null) {
+ Iterator iterator = selectedNodes.iterator();
+ while (iterator.hasNext()) {
+ IDirectoryNodeButton node = iterator.next();
+ iterator.remove();
+ node.setSelected(false);
+ }
+ }
+ }
+
+ public List getSelectedNodes() {
+ return selectedNodes;
+ }
+
+ public int countSelectedNodes() {
+ return selectedNodes.size();
+ }
+
+ public void deleteSelectedNodes() throws SftpException {
+
+ List filesToDelete = new ArrayList();
+ List directoriesToDelete = new ArrayList();
+
+ for(IDirectoryNodeButton node : selectedNodes) {
+ if(node.getNode().getAttrs().isDir()) {
+ directoriesToDelete.add(Helper.combinePath(getCurrentWorkingDirectory(), node.getNode().getFilename()).replace("\"", "\\\""));
+ }
+ else {
+ filesToDelete.add(Helper.combinePath(getCurrentWorkingDirectory(), node.getNode().getFilename()).replace("\"", "\\\""));
+ }
+ }
+
+ SshEngine.rm(filesToDelete);
+ SshEngine.rmdir(directoriesToDelete);
+
+ clearSelectedNodes();
+ }
+
+ public void downloadSelectedNodes(String destinationPath) {
+ List directories = new ArrayList();
+ List files = new ArrayList();
+ String tmp;
+ for(IDirectoryNodeButton node : selectedNodes) {
+ tmp = Helper.combinePath(getCurrentWorkingDirectory(), node.getNode().getFilename());
+ if(node.getNode().getAttrs().isDir()) {
+ directories.add(tmp);
+ }
+ else {
+ files.add(tmp);
+ }
+ }
+
+ for(String dir : directories) {
+ SshEngine.downloadDirectoryRecursively(dir, destinationPath);
+ }
+
+ for(String file : files) {
+ SshEngine.downloadFile(file, destinationPath);
+ }
+ }
+
+ /*
+ * ========== END Selected Nodes section ==========
+ */
+
+
+ /*
+ * ========== BEGIN CutCopyPasteController controller ==========
+ */
+
+ public class CutCopyPasteController{
+ private List sources = new ArrayList();
+ private int selectedOperation = Constants.Constants_FSOperations.NONE;
+
+ public void startCopying(List selectedNodes, String currentPath) {
+ String fullPath = null;
+ for(IDirectoryNodeButton nodeBtn : selectedNodes) {
+ fullPath = Helper.combinePath(currentPath, nodeBtn.getNode().getFilename());
+ sources.add(fullPath);
+ }
+ selectedOperation = Constants.Constants_FSOperations.COPY;
+ }
+
+ public void startCuttying(List selectedNodes, String currentPath) {
+ String fullPath = null;
+ for(IDirectoryNodeButton nodeBtn : selectedNodes) {
+ fullPath = Helper.combinePath(currentPath, nodeBtn.getNode().getFilename());
+ sources.add(fullPath);
+ }
+ selectedOperation = Constants.Constants_FSOperations.CUT;
+ }
+
+ public void paste(String destination) {
+ StringBuilder command = null;
+
+ // no source
+ if(sources.size() == 0) {
+ return;
+ }
+
+ // cannot write on destination
+ // we keep using isWriteable as
+ // the executeCommand() does not fire
+ // an exception in case of fail
+ if(!isWriteable(destination)) {
+ return;
+ }
+
+ // copy
+ if(selectedOperation == Constants.Constants_FSOperations.COPY) {
+ command = new StringBuilder("cp -r");
+
+ }
+
+ // cut
+ else if(selectedOperation == Constants.Constants_FSOperations.CUT) {
+ command = new StringBuilder("mv");
+ }
+
+ // invalid command
+ else {
+ return;
+ }
+
+ // execute
+ for(String path : sources) {
+ command.append(' ');
+ command.append('"');
+ command.append(path.replace("\"", "\\\""));
+ command.append('"');
+ }
+ command.append(' ');
+ command.append('"');
+ command.append(destination.replace("\"", "\\\""));
+ command.append('"');
+ SshEngine.executeCommand(command.toString());
+ selectedOperation = Constants_FSOperations.NONE;
+ }
+
+ public int getSelectedOperation() {
+ return selectedOperation;
+ }
+ }
+
+ /*
+ * ========== END CutCopyPasteController controller ==========
+ */
+
+
+ /*
+ * ========== BEGIN File System Operations ==========
+ */
+
+
+ /**
+ * Creates a new folder
+ * @param newFolderPath Folder's path
+ * @throws SftpException
+ */
+ public void mkdir(String newFolderPath) throws SftpException {
+ SshEngine.mkdir(newFolderPath);
+ }
+
+ /**
+ * Creates a file in the remote file path
+ * @param remoteFilePath remote file path
+ * @throws SftpException
+ */
+ public void touch(String remoteFilePath) throws SftpException {
+ SshEngine.touch(remoteFilePath);
+ }
+
+ /**
+ * Renames a file
+ * @param oldNamePath Path of the old name
+ * @param newNamePath Path of the new name
+ * @throws SftpException
+ */
+ public void rename(String oldNamePath, String newNamePath) throws SftpException {
+ SshEngine.rename(oldNamePath, newNamePath);
+ }
+
+ /*
+ * ========== END File System Operations ==========
+ */
+
+ /*
+ * ========== BEGIN Other ==========
+ */
+
+ /**
+ * Given a remote file path, opens a graphical notepad for it
+ * @param filePath remote file path to display in the notepad
+ */
+ public void openNotepadForFile(String filePath) {
+ new NotepadController(filePath).show();
+ }
+
+ /**
+ * Disposes resources which need to be freed before exiting
+ * the application
+ */
+ public void disposeResources() {
+ SshEngine.disconnectSession();
+ }
+
+ /**
+ * @deprecated This method is deprecated.
+ * Catch SftpException
+ * and look for "Permission denied" instead. This prevents
+ * unnecessary overhead
+ */
+ public boolean isReadable(String path) {
+ StringBuilder command = new StringBuilder();
+ command.append("[ -r \"");
+ command.append(path.equals("~") ? SshEngine.executeCommand("pwd").replace("\"", "\\\"") : path.replace("\"", "\\\""));
+ command.append("\" ] && echo 1 || echo 0"); // short circuiting
+ return SshEngine.executeCommand(command.toString()).trim().equals("1");
+ }
+
+ /**
+ * @deprecated This method is deprecated.
+ * Catch SftpException
+ * and look for "Permission denied" instead
+ */
+ public boolean isWriteable(String path) {
+ StringBuilder command = new StringBuilder();
+ command.append("[ -w \"");
+ command.append(path.equals("~") ? SshEngine.executeCommand("pwd").replace("\"", "\\\"") : path.replace("\"", "\\\""));
+ command.append("\" ] && echo 1 || echo 0"); // short circuiting
+ return SshEngine.executeCommand(command.toString()).trim().equals("1");
+ }
+
+ public void showFrame(boolean show) {
+ frame.setVisible(show);
+ }
+
+ /*
+ * ========== END Other ==========
+ */
+
+}
diff --git a/Guify/src/controllers/FindAndReplaceController.java b/Guify/src/controllers/FindAndReplaceController.java
new file mode 100644
index 0000000..65b4926
--- /dev/null
+++ b/Guify/src/controllers/FindAndReplaceController.java
@@ -0,0 +1,86 @@
+package controllers;
+
+import code.GuiAbstractions.Implementations.JFrameFactory;
+import code.GuiAbstractions.Interfaces.*;
+import views.interfaces.IFindAndReplaceFrame;
+import views.interfaces.INotepadFrame;
+
+public class FindAndReplaceController {
+
+ private IGenericTextArea textArea;
+ private IFindAndReplaceFrame frame;
+
+ public FindAndReplaceController(IGenericTextArea textArea) {
+ this.textArea = textArea;
+ try {
+ frame = (IFindAndReplaceFrame) JFrameFactory.createJFrame(IFrameFactory.FIND_AND_REPLACE, this);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Show frame centered to parent
+ */
+ public void showAtTheCenterOfFrame(INotepadFrame notepadFrame) {
+ int childX = notepadFrame.getX() + (notepadFrame.getWidth() - frame.getWidth()) / 2;
+ int childY = notepadFrame.getY() + (notepadFrame.getHeight() - frame.getHeight()) / 2;
+ frame.setLocation(childX, childY);
+ frame.setVisible(true);
+ }
+
+ public int findNext(String searchText) {
+ String text = textArea.getText();
+ int currentIndex = textArea.getCaretPosition();
+ int nextIndex = text.indexOf(searchText, currentIndex);
+
+ if (nextIndex != -1) {
+ textArea.selectText(nextIndex, nextIndex + searchText.length());
+ return nextIndex;
+ }
+ return -1;
+ }
+
+ public int findPrevious(String searchText) {
+ String text = textArea.getText();
+ int cutAt;
+
+ if(textArea.hasHighlightedText()) {
+ cutAt = textArea.getSelectionStart();
+ }
+ else {
+ cutAt = textArea.getCaretPosition();
+ }
+ String firstPart = text.substring(0, cutAt);
+ int previousIndex = firstPart.lastIndexOf(searchText, firstPart.length() - 1);
+ if (previousIndex != -1) {
+ textArea.selectText(previousIndex, previousIndex + searchText.length());
+ return previousIndex;
+ }
+ else {
+ return -1;
+ }
+ }
+
+ public int replaceNext(String toReplace, String replaceWith) {
+ int index = findNext(toReplace);
+
+ if(index != -1) {
+ textArea.replaceRange(replaceWith, index, index + toReplace.length());
+ }
+
+ return index;
+ }
+
+ public void replaceAll(String searchText, String replacement) {
+ String text = textArea.getText();
+ text = text.replaceAll(searchText, replacement);
+ textArea.setText(text);
+ }
+
+ public void disposeMyFrame() {
+ if(frame != null) {
+ frame.dispose();
+ }
+ }
+}
diff --git a/Guify/src/controllers/LoginController.java b/Guify/src/controllers/LoginController.java
new file mode 100644
index 0000000..c1fdf6a
--- /dev/null
+++ b/Guify/src/controllers/LoginController.java
@@ -0,0 +1,69 @@
+package controllers;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import code.SshEngine;
+import code.GuiAbstractions.Implementations.JFrameFactory;
+import code.GuiAbstractions.Interfaces.IFrameFactory;
+import views.interfaces.ILoginFrame;
+
+public class LoginController {
+
+ private ILoginFrame frame;
+
+ public LoginController() {
+ try {
+ frame = (ILoginFrame) JFrameFactory.createJFrame(IFrameFactory.LOGIN, this);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public boolean Login(String host, String username, String password, String port) throws IllegalArgumentException {
+ LoginCredentials.host = host;
+ LoginCredentials.username = username;
+ LoginCredentials.password = password;
+ LoginCredentials.port = Integer.parseInt(port);
+
+ if (SshEngine.connetion()) {
+ frame.setVisible(false);
+ new DesktopController().showFrame(true);;
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ public void ValidateInput(String host, String username, String password, String port) throws IllegalArgumentException {
+
+ // Host Validation. Consider its necessity.
+ try {
+ InetAddress.getByName(host);
+ } catch (UnknownHostException ex) {
+ throw new IllegalArgumentException("Host could not be found", ex);
+ }
+
+ // Port Validation
+ try {
+ Integer.parseInt(port);
+ }
+ catch(NumberFormatException ex) {
+ throw new IllegalArgumentException("Invalid port number", ex);
+ }
+ }
+
+ public void showFrame(boolean show) {
+ frame.setVisible(show);
+ }
+
+ public static class LoginCredentials{
+ public static String host;
+ public static String username;
+ public static String password;
+ public static int port;
+ }
+
+}
diff --git a/Guify/src/controllers/NotepadController.java b/Guify/src/controllers/NotepadController.java
new file mode 100644
index 0000000..57b5aac
--- /dev/null
+++ b/Guify/src/controllers/NotepadController.java
@@ -0,0 +1,60 @@
+package controllers;
+
+import code.SshEngine;
+import code.GuiAbstractions.Implementations.JFrameFactory;
+import code.GuiAbstractions.Interfaces.IFrameFactory;
+import code.GuiAbstractions.Interfaces.IGenericTextArea;
+import views.interfaces.INotepadFrame;
+public class NotepadController {
+
+ private String filePath = null;
+ private INotepadFrame notepadFrame = null;
+ private FindAndReplaceController myFindAndReplaceController;
+ private boolean unsaved = false;
+
+ public boolean isUnsaved() {
+ return unsaved;
+ }
+
+ public void setUnsaved(boolean unsaved) {
+ this.unsaved = unsaved;
+ }
+
+ public String getFilePath() {
+ return this.filePath;
+ }
+
+ public NotepadController(String filePath) {
+ this.filePath = filePath;
+ String contentToDisplay = SshEngine.readFile(filePath);
+ try {
+ notepadFrame = (INotepadFrame) JFrameFactory.createJFrame(IFrameFactory.NOTEPAD, this);
+ notepadFrame.displayContent(contentToDisplay);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return;
+ }
+ }
+
+ public void show() {
+ notepadFrame.setVisible(true);
+ }
+
+ public void writeOnFile(String text) {
+ SshEngine.writeFile(text, this.filePath);
+ }
+
+ public void showFindAndReplace(IGenericTextArea textArea) {
+ myFindAndReplaceController = new FindAndReplaceController(textArea);
+ myFindAndReplaceController.showAtTheCenterOfFrame(notepadFrame);
+ }
+
+ public void disposeFindAndReplaceFrame() {
+ if(myFindAndReplaceController != null)
+ myFindAndReplaceController.disposeMyFrame();
+ }
+
+ public String getTitle() {
+ return filePath + (unsaved ? " - UNSAVED" : "");
+ }
+}
diff --git a/Guify/src/controllers/QueueController.java b/Guify/src/controllers/QueueController.java
new file mode 100644
index 0000000..14aafe2
--- /dev/null
+++ b/Guify/src/controllers/QueueController.java
@@ -0,0 +1,168 @@
+package controllers;
+
+import java.util.Observable;
+import java.util.Observer;
+import code.QueueEventManager;
+import code.TransferProgress;
+import code.GuiAbstractions.Implementations.JFrameFactory;
+import code.GuiAbstractions.Interfaces.IFrameFactory;
+import views.interfaces.IQueueFrame;
+
+import java.util.concurrent.ConcurrentHashMap;
+import javax.swing.SwingUtilities;
+
+import com.jcraft.jsch.SftpProgressMonitor;
+
+@SuppressWarnings("deprecation") // Observer/Observable objects are okay here
+public class QueueController implements Observer {
+
+ private IQueueFrame frame;
+ // A HashMap containing the Transfer Progress entry and the index of said entry
+ // in the table.
+ // We need a ConcurrentHashMap instead of a simple HashMap because it
+ // is accessed by multiple threads. In particular the threads executing update()
+ // and the Event Dispatch Thread.
+ // Alternatively, the HashMap could've been managed by the view itself without the need
+ // to concern over threading/concurrent problems.
+ // TODO Make the HashMap to be handled by the view itself (EDT thread) rather than concurrent threads
+ // to enhance readability and understanding
+ private ConcurrentHashMap indexAssociationMap = new ConcurrentHashMap<>();
+ private final static int HASHMAP_DUMMY_VALUE = -1;
+
+ // Executed by the EDT
+ public QueueController() {
+ try {
+ frame = (IQueueFrame) JFrameFactory.createJFrame(IFrameFactory.QUEUE);
+ }
+ catch (Exception e) {}
+
+ // Register observer of the changes
+ QueueEventManager.getInstance().addObserver(this);
+
+ // Get previous enqueued elements. Do not place before addObserver(this) or some
+ // transfers could go lost
+ TransferProgress[] queued = QueueEventManager.getInstance().getQueue();
+ for(TransferProgress transferProgress : queued) {
+ // It is possible that while iterating on this for, the element
+ // has already been inserted into the indexAssociationMap from
+ // another thread executing update(), hence, we check if the key is contained
+ // already.
+ if (indexAssociationMap.putIfAbsent(transferProgress, HASHMAP_DUMMY_VALUE) == null) {
+ int percentage = (int) Math.floor( ((transferProgress.getTransferredBytes() * 100) / transferProgress.getTotalBytes()) );
+ int rowIndex = frame.addRow(transferProgress.getSource(), transferProgress.getDestination(), transferProgress.getOperation() == SftpProgressMonitor.GET? "Download" : "Upload", percentage);
+ indexAssociationMap.put(transferProgress, rowIndex);
+ }
+ }
+ }
+
+ // Updated by QueueEventManager. Can run simultaneously
+ // on multiple threads.
+ @Override
+ public void update(Observable o, Object arg) {
+ TransferProgress transferProgressObj = (TransferProgress)arg;
+ if(transferProgressObj.getTransferStatus() == TransferProgress.INIT) {
+ // Since the Runnable in SwingUtilities.invokeLater contained in this if
+ // might run *after* a subsequent execution of
+ // update() having a TransferProgress whose status is UPDATING, said subsequent update()
+ // must know that this specific transferProgressObj was already inserted in the HashMap,
+ // otherwise the if (indexAssociationMap.putIfAbsent(transferProgressObj, HASHMAP_DUMMY_VALUE) == null)
+ // (contained in the if checking whether the status is UPDATING)
+ // would return true and add it again to the table, because indexAssociationMap.put(transferProgressObj, rowIndex);
+ // has not been completed yet, being in the Runnable.
+ //
+ // We call putIfAbsent() instead of just put() because it is possible that this transferProgressObj is also
+ // in the initial for present in Queue() if this transfer was initiated in a time x where
+ // t1 < x < t2, where t1 is the time of completion of QueueEventManager.getInstance().addObserver(this);
+ // and t2 is the time of completion of QueueEventManager.getInstance().getQueue();
+ //
+ // To sum it up:
+ // 1) update() receives transferProgressObj whose status is "INIT". Puts "DUMMY_VALUE"
+ // and schedules the EDT for a table insertion;
+ // 2) update() receives the same transferProgressObj with the status "UPDATING".
+ // It successfully sees that indexAssociationMap contains this specific transferProgressObj,
+ // whose value is DUMMY_VALUE, so it does not schedule an insert, but rather an update. While
+ // updating it will see that the value is not valid (if(rowIndex != HASHMAP_DUMMY_VALUE)) and will
+ // not perform an update as well.
+ // 3) The EDT runs and puts the correct index in the HashMap.
+ //
+ // Remember that if an update() receives a transferProgress whose status is INIT, that update() will always run
+ // before an update() on the same transferProgress whose status is UPDATING, as the thread handling
+ // each transferProgress is one and one only.
+ if(indexAssociationMap.putIfAbsent(transferProgressObj, HASHMAP_DUMMY_VALUE) == null) {
+ // We need SwingUtilities.invokeLater because we
+ // are not on the Event Dispatch Thread (the thread
+ // responsible for GUI management), but rather on the
+ // thread created in SshEngine
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ int rowIndex = frame.addRow(transferProgressObj.getSource(), transferProgressObj.getDestination(), transferProgressObj.getOperation() == SftpProgressMonitor.GET? "Download" : "Upload", 0);
+ indexAssociationMap.put(transferProgressObj, rowIndex);
+ }
+ });
+ }
+ }
+ else if(transferProgressObj.getTransferStatus() == TransferProgress.UPDATING) {
+ int percentage;
+ // Avoid division by zero
+ if(transferProgressObj.getTotalBytes() == 0) {
+ // The percentage is 100% if ∀ byte in the file, byte was transferred.
+ // If there are no bytes in the file, this logic proposition holds true (vacuous truth)
+ percentage = 100;
+ }
+ else {
+ percentage = (int) Math.floor( ((transferProgressObj.getTransferredBytes() * 100) / transferProgressObj.getTotalBytes()) );
+ }
+ // It is possible to receive TransferProgress.UPDATING without receiving
+ // a TransferProgress.INIT (when this controller gets created when the transferring
+ // was already occurring) and before "for(TransferProgress transferProgress : queued)"
+ // gets executed on this element
+ if (indexAssociationMap.putIfAbsent(transferProgressObj, HASHMAP_DUMMY_VALUE) == null) {
+ // We need SwingUtilities.invokeLater because we
+ // are not on the Event Dispatch Thread (the thread
+ // responsible for GUI management), but rather on the
+ // thread created in SshEngine
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ int rowIndex = frame.addRow(transferProgressObj.getSource(), transferProgressObj.getDestination(), transferProgressObj.getOperation() == SftpProgressMonitor.GET? "Download" : "Upload", percentage);
+ indexAssociationMap.put(transferProgressObj, rowIndex);
+ }
+ });
+ }
+ else {
+ int rowIndex = indexAssociationMap.get(transferProgressObj);
+ // It is possible that rowIndex is a DUMMY_VALUE if the insertion
+ // into the table has been scheduled with SwingUtilities.invokeLater
+ // but has not ran yet
+ if(rowIndex != HASHMAP_DUMMY_VALUE) {
+ // We need SwingUtilities.invokeLater because we
+ // are not on the Event Dispatch Thread (the thread
+ // responsible for GUI management), but rather on the
+ // thread created in SshEngine
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ frame.updateRow(rowIndex, percentage);
+ }
+ });
+ }
+ else {
+ // TODO If the file is small enough, in all the (few)
+ // updates, rowIndex could always be HASHMAP_DUMMY_VALUE
+ // because when SwingUtilities tries to insert the row the first time,
+ // the EDT task will not execute soon enough before the termination
+ // of the transfer, so we will see that the percentage it's fixed
+ // to a specific value.
+ }
+ }
+ }
+ else if(transferProgressObj.getTransferStatus() == TransferProgress.END) {
+ // We choose not to remove the element from the table once it has finished
+ }
+ }
+
+ public void showFrame(boolean visible) {
+ frame.setVisible(true);
+ }
+}
diff --git a/Guify/src/views/Desktop.java b/Guify/src/views/Desktop.java
new file mode 100644
index 0000000..2d4c94c
--- /dev/null
+++ b/Guify/src/views/Desktop.java
@@ -0,0 +1,1070 @@
+package views;
+
+import javax.swing.JFrame;
+import controllers.QueueController;
+import views.interfaces.IDesktopFrame;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.TreePath;
+import com.jcraft.jsch.ChannelSftp.LsEntry;
+import com.jcraft.jsch.SftpException;
+import code.Constants;
+import code.Constants.GuifyColors;
+import code.TreeNode;
+import code.Helper;
+import code.JDirectoryNodeButton;
+import code.WrapLayout;
+import controllers.DesktopController;
+import javax.swing.JTree;
+import javax.swing.JPopupMenu;
+import javax.swing.JTextField;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.LineBorder;
+import javax.swing.JFileChooser;
+import javax.swing.JMenuItem;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Vector;
+import javax.swing.JScrollPane;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.dnd.DropTarget;
+import java.awt.dnd.DropTargetDragEvent;
+import java.awt.dnd.DropTargetDropEvent;
+import java.awt.dnd.DropTargetEvent;
+import java.awt.dnd.DropTargetListener;
+import javax.imageio.ImageIO;
+import javax.swing.Box;
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import javax.swing.JButton;
+import java.awt.*;
+import javax.swing.JToolBar;
+
+public class Desktop extends JFrame implements IDesktopFrame {
+
+ /*
+ * ========== BEGIN Attributes ==========
+ */
+
+ private static final long serialVersionUID = 1L;
+ private DesktopController controller;
+ private JTree tree;
+ private JScrollPane treePanel;
+ private JPanel desktopPanel;
+ private JToolBar toolBar;
+ private JButton cutBtn;
+ private JButton copyBtn;
+ private JButton pasteBtn;
+ private JButton renameBtn;
+ private JButton deleteBtn;
+ private JButton downloadBtn;
+ private JTextField pathTextBox;
+
+ /*
+ * ========== END Attributes ==========
+ */
+
+
+ /*
+ * ========== BEGIN Constructors ==========
+ */
+
+ public Desktop(Object controller) {
+ this.controller = (DesktopController) controller;
+
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setBounds(100, 100, 1280, 720);
+ getContentPane().setLayout(null);
+ setTitle(Constants.APP_NAME);
+
+ treePanel = new JScrollPane();
+ treePanel.setBounds(0, 36, 150, 634);
+ getContentPane().add(treePanel);
+
+ JScrollPane scrollPane = new JScrollPane();
+ scrollPane.setBounds(156, 36, 1098, 634);
+ scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+ getContentPane().add(scrollPane);
+
+ desktopPanel = new JPanel();
+ scrollPane.setViewportView(desktopPanel);
+ desktopPanel.setLayout(new WrapLayout(FlowLayout.LEFT, 5, 5));
+ desktopPanel.setBackground(Color.WHITE);
+ desktopPanel.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e){
+ unselectAllNodes();
+ desktopPanel.requestFocus();
+ }
+ });
+
+ toolBar = new JToolBar();
+ toolBar.setFloatable(false);
+ toolBar.setBounds(0, 0, 614, 37);
+ toolBar.setBackground(GuifyColors.GRAY);
+ createJToolBar();
+ getContentPane().add(toolBar);
+
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ ((DesktopController) controller).disposeResources();
+ }
+ });
+
+ // Create drag and drop handler
+ new DropTarget(desktopPanel, new DropTargetListener() {
+
+ @Override
+ public void dragEnter(DropTargetDragEvent dtde) {
+ }
+
+ @Override
+ public void dragOver(DropTargetDragEvent dtde) {
+ desktopPanel.setBorder(new LineBorder(GuifyColors.BLUE, 2));
+
+ }
+
+ @Override
+ public void dropActionChanged(DropTargetDragEvent dtde) {
+ }
+
+ @Override
+ public void dragExit(DropTargetEvent dte) {
+ desktopPanel.setBorder(null);
+
+ }
+
+ @Override
+ public void drop(DropTargetDropEvent dtde) {
+ dtde.acceptDrop(dtde.getDropAction());
+ Transferable data = dtde.getTransferable();
+ if (data.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
+ try {
+ File[] droppedFileArray = ((Collection>)(data.getTransferData(DataFlavor.javaFileListFlavor))).toArray(new File[((Collection>)(data.getTransferData(DataFlavor.javaFileListFlavor))).size()]);
+ try {
+ ((DesktopController)controller).uploadToRemoteServer(droppedFileArray);
+ drawComponentsForDirectory(((DesktopController)controller).getCurrentWorkingDirectory());
+ } catch (SftpException e) {
+ if (e.getMessage().contains("Permission denied")) {
+ JOptionPane.showMessageDialog(new JFrame(), "Permission denied", "Permission denied", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+ }
+ } catch (UnsupportedFlavorException | IOException e) {
+ e.printStackTrace();
+ }
+ }
+ dtde.dropComplete(true);
+ desktopPanel.setBorder(null);
+ }});
+ }
+
+ /*
+ * ========== END Constructors ==========
+ */
+
+
+ /*
+ * ========== BEGIN Frame Drawing ==========
+ */
+
+ /**
+ * Draws all the components which need to be drew for a specific
+ * directory
+ * @param directory
+ */
+ public void drawComponentsForDirectory(String directory) {
+ try {
+ controller.clearSelectedNodes();
+ updateToolBarItems();
+ controller.setCurrentWorkingDirectory(directory);
+ pathTextBox.setText(controller.getCurrentWorkingDirectory());
+ loadTree();
+ loadDesktop();
+ }catch(Exception ex) {
+ if(controller.getLastSafeDirectory() == null) {
+ System.exit(ERROR);
+ }
+ else {
+ drawComponentsForDirectory(controller.getLastSafeDirectory());
+ controller.setLastSafeDirectory(null); // Prevents infinite re-tries
+ return;
+ }
+ }
+ controller.setLastSafeDirectory(directory);
+
+ repaint();
+ revalidate();
+ }
+
+ /**
+ * Loads the desktop view
+ * @throws SftpException
+ */
+ private void loadDesktop() throws SftpException {
+ desktopPanel.removeAll();
+
+ Image folderIcon = null;
+ Image fileIcon = null;
+ try {
+ folderIcon = ImageIO.read(getClass().getClassLoader().getResource("folder_icon.png")).getScaledInstance(32, 32, Image.SCALE_DEFAULT);
+ fileIcon = ImageIO.read(getClass().getClassLoader().getResource("file_icon.png")).getScaledInstance(32, 32, Image.SCALE_DEFAULT);
+ } catch (IOException e1) {}
+
+ Vector elementsToDisplay = null;
+ try {
+ elementsToDisplay = controller.getDirectoryElements();
+ if(elementsToDisplay == null) {
+ return;
+ }
+ } catch (SftpException e) {
+ if (e.getMessage().contains("Permission denied")) {
+ JOptionPane.showMessageDialog(new JFrame(), "Permission denied", "Permission denied", JOptionPane.ERROR_MESSAGE);
+ throw e;
+ }
+ }
+
+ for(LsEntry node : elementsToDisplay) {
+ ImageIcon icon = new ImageIcon( node.getAttrs().isDir()? folderIcon : fileIcon );
+ JDirectoryNodeButton element = new JDirectoryNodeButton(node);
+ JLabel iconLabel = new JLabel(icon);
+ iconLabel.setVerticalTextPosition(JLabel.BOTTOM);
+ iconLabel.setHorizontalTextPosition(JLabel.CENTER);
+ iconLabel.setText(node.getFilename());
+ element.add(iconLabel);
+ element.setBackground(new Color(255, 255, 255));
+ element.setToolTipText(node.getFilename());
+ int buttonWidth = 75;
+ int buttonHeight = element.getPreferredSize().height;
+ Dimension buttonSize = new Dimension(buttonWidth, buttonHeight);
+ element.setPreferredSize(buttonSize);
+ element.setMaximumSize(buttonSize);
+ element.setMinimumSize(buttonSize);
+ element.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e){
+ desktopElementClick(e);
+ }
+ });
+ desktopPanel.add(element);
+ }
+ }
+
+ /**
+ * Specifies the action to take upon a click on the element
+ * of a desktop
+ * @param e A MouseEvent
+ */
+ private void desktopElementClick(MouseEvent e) {
+ JDirectoryNodeButton sender = (JDirectoryNodeButton) e.getSource();
+
+ // Move into directory
+ if(e.getClickCount() == 2){
+ ImageIcon questionMarkIcon = null;
+ try {
+ questionMarkIcon = new ImageIcon(
+ ImageIO.read(getClass().getClassLoader().getResource("question_mark.png")).getScaledInstance(32, 32, Image.SCALE_SMOOTH));
+ }
+ catch (IOException e1) {}
+
+ // Double click on a directory
+ if(sender.node.getAttrs().isDir()) {
+ String newDirectory = Helper.combinePath(controller.getCurrentWorkingDirectory(), sender.node.getFilename());
+ drawComponentsForDirectory(newDirectory);
+ }
+
+ // Double click on a file
+ else {
+ String filePath = Helper.combinePath(controller.getCurrentWorkingDirectory(), sender.node.getFilename());
+ Object[] options = {"Download", "View", "Cancel"};
+
+ int choice = JOptionPane.showOptionDialog(null,
+ "What would you like to do with this file?",
+ Constants.APP_NAME,
+ JOptionPane.YES_NO_CANCEL_OPTION,
+ JOptionPane.QUESTION_MESSAGE,
+ questionMarkIcon,
+ options,
+ options[0]);
+
+ switch(choice) {
+
+ // Download
+ case 0:
+
+ JFileChooser fileChooser = new JFileChooser();
+ fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ int choiceFileChooser = fileChooser.showDialog(this, "Save here");
+
+ if (choiceFileChooser == JFileChooser.APPROVE_OPTION) {
+ controller.downloadFile(filePath, fileChooser.getSelectedFile().toString());
+ }
+ break;
+
+ // View
+ case 1:
+ controller.openNotepadForFile(filePath);
+ break;
+
+ // Cancel
+ case 2:
+ return;
+ }
+
+ }
+ }
+
+ // Select a node
+ else if(e.getClickCount() == 1) {
+
+ boolean isControlDown = (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0;
+
+ // If already selected, unselect
+ if(sender.getSelected()) {
+ if(!isControlDown) {
+ unselectNode(sender);
+ }
+ }
+
+ // If not selected, select
+ else {
+ if(!isControlDown) {
+ // Unselect all the other components
+ unselectAllNodes();
+ }
+
+ // Select the current component
+ selectNode(sender);
+ }
+ }
+ }
+
+ /**
+ * Loads the file system tree seen
+ * on the left
+ */
+ private void loadTree() {
+ TreeNode root = this.controller.getTree();
+ DefaultTreeModel model = new DefaultTreeModel(loadTreeAux(root), true);
+ tree = new JTree(model);
+
+ // Click on the tree. Open directory if directory is clicked,
+ // do nothing otherwise.
+ tree.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ TreePath path = tree.getPathForLocation(e.getX(), e.getY());
+ if (path != null) {
+ DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
+ if(node.getAllowsChildren()) { // is a directory
+ String fullPath = getFullPathFromTreeNode(node);
+ drawComponentsForDirectory(fullPath);
+ }
+ else {
+ // Do nothing. Probably a "What to do?" prompt would
+ // be nice
+ }
+ }
+ }
+ }
+ });
+ treePanel.setViewportView(tree);
+ }
+
+ /**
+ * Auxiliary function which recursively
+ * creates a tree given its root
+ * @param node A TreeNode element
+ * @return A DefaultMutableTreeNode element representing the root
+ * of the created tree
+ */
+ private DefaultMutableTreeNode loadTreeAux(TreeNode node) {
+
+ if(node == null)
+ return null;
+
+ if(node.error != null)
+ return null;
+
+ DefaultMutableTreeNode treeNode = new DefaultMutableTreeNode(node.name, node.type.equals("directory"));
+
+ if(node.contents != null) {
+ for(TreeNode child : node.contents) {
+ DefaultMutableTreeNode descendants = loadTreeAux(child);
+ if(descendants != null) {
+ treeNode.add(descendants);
+ }
+ }
+ }
+
+ return treeNode;
+ }
+
+ /**
+ * Retrieves the full path in the file system of a specified
+ * tree node
+ * @param node A DefaultMutableTreeNode element
+ * @return Full path of the node
+ */
+ private String getFullPathFromTreeNode(DefaultMutableTreeNode node) {
+ javax.swing.tree.TreeNode[] path = node.getPath();
+ StringBuilder fullPath = new StringBuilder();
+ for (int i = 0; i < path.length; i++) {
+ Object userObject = ((DefaultMutableTreeNode) path[i]).getUserObject();
+ fullPath.append(userObject.toString());
+ if (i != path.length - 1) {
+ fullPath.append("/");
+ }
+ }
+ return fullPath.toString();
+ }
+
+ /**
+ * Creates the tool bar
+ */
+ private void createJToolBar() {
+ JButton backBtn = new JButton();
+ backBtn.setBorderPainted(false);
+ backBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border;
+ backBtn.setToolTipText("Back");
+ try {
+ backBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("back_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ backBtn.setBorderPainted(false);
+ }catch(IOException ex) {
+ backBtn.setText("Back");
+ }
+ backBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if(controller.getCurrentWorkingDirectory().equals("/")) {
+ return;
+ }
+ String parentPath = Helper.getParentPath(controller.getCurrentWorkingDirectory());
+ drawComponentsForDirectory(parentPath);
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(backBtn.isEnabled())
+ backBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ backBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ cutBtn = new JButton();
+ cutBtn.setBorderPainted(false);
+ cutBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border;
+ cutBtn.setToolTipText("Cut");
+ try {
+ cutBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("cut_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ cutBtn.setBackground(GuifyColors.GRAY);
+ cutBtn.setBorderPainted(false);
+ cutBtn.setEnabled(false);
+ }catch(IOException ex) {
+ cutBtn.setText("Cut");
+ }
+ cutBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if(cutBtn.isEnabled()) {
+ controller.cutCopyPasteController.startCuttying(controller.getSelectedNodes(), controller.getCurrentWorkingDirectory());
+ pasteBtn.setEnabled(true);
+ }
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(cutBtn.isEnabled())
+ cutBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ cutBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ copyBtn = new JButton();
+ copyBtn.setBorderPainted(false);
+ copyBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border;
+ copyBtn.setToolTipText("Copy");
+ try {
+ copyBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("copy_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ copyBtn.setBackground(GuifyColors.GRAY);
+ copyBtn.setBorderPainted(false);
+ copyBtn.setEnabled(false);
+ }catch(IOException ex) {
+ copyBtn.setText("Copy");
+ }
+ copyBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ controller.cutCopyPasteController.startCopying(controller.getSelectedNodes(), controller.getCurrentWorkingDirectory());
+ pasteBtn.setEnabled(true);
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(copyBtn.isEnabled())
+ copyBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ copyBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ pasteBtn = new JButton();
+ pasteBtn.setBorderPainted(false);
+ pasteBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border;
+ pasteBtn.setToolTipText("Paste");
+ try {
+ pasteBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("paste_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ pasteBtn.setBorderPainted(false);
+ pasteBtn.setEnabled(false);
+ }catch(IOException ex) {
+ pasteBtn.setText("Paste");
+ }
+ pasteBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ controller.cutCopyPasteController.paste(controller.getCurrentWorkingDirectory());
+ drawComponentsForDirectory(controller.getCurrentWorkingDirectory());
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(pasteBtn.isEnabled())
+ pasteBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ pasteBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ renameBtn = new JButton();
+ renameBtn.setBorderPainted(false);
+ renameBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border;
+ renameBtn.setToolTipText("Rename");
+ try {
+ renameBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("rename_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ renameBtn.setBackground(GuifyColors.GRAY);
+ renameBtn.setBorderPainted(false);
+ renameBtn.setEnabled(false);
+ }catch(IOException ex) {
+ renameBtn.setText("Rename");
+ }
+ renameBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+
+ // Something's off and the rename button shouldn't have been active in the first place
+ if(controller.getSelectedNodes().size() != 1) {
+ return;
+ }
+ String oldPath = Helper.combinePath(controller.getCurrentWorkingDirectory(), controller.getSelectedNodes().get(0).getNode().getFilename());
+
+ String newName = (String)JOptionPane.showInputDialog(
+ Desktop.this,
+ "Rename \"" + controller.getSelectedNodes().get(0).getNode().getFilename() + "\"",
+ "Rename",
+ JOptionPane.PLAIN_MESSAGE,
+ null,
+ null,
+ null);
+
+ // has closed or canceled
+ if(newName == null) {
+ return;
+ }
+
+ String newPath = Helper.combinePath(controller.getCurrentWorkingDirectory(), newName);
+ try {
+ controller.rename(oldPath, newPath);
+ } catch (SftpException e1) {
+ if (e1.getMessage().contains("Permission denied")) {
+ JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to rename this element", "Permission denied", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+ }
+ drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); // TODO optimize this
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(renameBtn.isEnabled())
+ renameBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ renameBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ JButton newBtn = new JButton();
+ newBtn.setBorderPainted(false);
+ newBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border);
+ newBtn.setToolTipText("New");
+ try {
+ newBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("plus_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ newBtn.setBackground(GuifyColors.GRAY);
+ newBtn.setBorderPainted(false);
+ }catch(IOException ex) {
+ newBtn.setText("New");
+ }
+ newBtn.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ JPopupMenu menu = new JPopupMenu();
+
+ JMenuItem newFileMenuItem = new JMenuItem("New file");
+ try {
+ newFileMenuItem.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("file_icon.png")).getScaledInstance(16, 16, Image.SCALE_SMOOTH)));
+ } catch (IOException e1) {
+
+ }
+ newFileMenuItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+
+ String newFileName = (String)JOptionPane.showInputDialog(
+ Desktop.this,
+ "Name:",
+ "New file",
+ JOptionPane.PLAIN_MESSAGE,
+ null,
+ null,
+ null);
+
+ String newFilePath = Helper.combinePath(controller.getCurrentWorkingDirectory(), newFileName);
+ try {
+ controller.touch(newFilePath);
+ } catch (SftpException e1) {
+ if (e1.getMessage().contains("Permission denied")) {
+ JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to create a file here", "Permission denied", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+ }
+ drawComponentsForDirectory(controller.getCurrentWorkingDirectory());
+ }
+ });
+
+ JMenuItem newFolderMenuItem = new JMenuItem("New folder");
+ try {
+ newFolderMenuItem.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("folder_icon.png")).getScaledInstance(16, 16, Image.SCALE_SMOOTH)));
+ } catch (IOException e1) {
+
+ }
+ newFolderMenuItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+
+ String newFolderName = (String)JOptionPane.showInputDialog(
+ Desktop.this,
+ "Name:",
+ "New folder",
+ JOptionPane.PLAIN_MESSAGE,
+ null,
+ null,
+ null);
+
+ // User has canceled
+ if(newFolderName == null) {
+ return;
+ }
+
+ String newFolderPath = Helper.combinePath(controller.getCurrentWorkingDirectory(), newFolderName);
+ try {
+ controller.mkdir(newFolderPath);
+ } catch (SftpException e1) {
+ if (e1.getMessage().contains("Permission denied")) {
+ JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to create a folder here", "Permission denied", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+ }
+ drawComponentsForDirectory(controller.getCurrentWorkingDirectory()); //TODO: avoid a complete desktop reload
+ }
+ });
+
+ menu.add(newFileMenuItem);
+ menu.add(newFolderMenuItem);
+ menu.show(newBtn, 0, newBtn.getHeight());
+ }
+ });
+ newBtn.addMouseListener(new MouseAdapter() {
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(newBtn.isEnabled())
+ newBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ newBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ deleteBtn = new JButton();
+ deleteBtn.setBorderPainted(false);
+ deleteBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border);
+ deleteBtn.setToolTipText("Delete");
+ try {
+ deleteBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("delete_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ deleteBtn.setBackground(GuifyColors.GRAY);
+ deleteBtn.setBorderPainted(false);
+ deleteBtn.setEnabled(false);
+ }catch(IOException ex) {
+ deleteBtn.setText("Delete");
+ }
+ deleteBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ ImageIcon deleteIcon = null;
+ try {
+ deleteIcon = new ImageIcon(
+ ImageIO.read(getClass().getClassLoader().getResource("delete_icon.png")).getScaledInstance(32, 32, Image.SCALE_SMOOTH));
+ }
+ catch (IOException e1) {}
+ int choice = JOptionPane.showOptionDialog(null,
+ "Do you really want to delete the selected items?",
+ Constants.APP_NAME,
+ JOptionPane.YES_NO_OPTION,
+ JOptionPane.QUESTION_MESSAGE,
+ deleteIcon,
+ null,
+ null);
+
+ if(choice == 0) { // yes
+ try {
+ controller.deleteSelectedNodes();
+ } catch (SftpException e1) {
+ if (e1.getMessage().contains("Permission denied")) {
+ JOptionPane.showMessageDialog(new JFrame(), "Deletion process has encountered an item which cannot be deleted", "Permission denied", JOptionPane.ERROR_MESSAGE);
+ }
+ }
+ drawComponentsForDirectory(controller.getCurrentWorkingDirectory());
+ }
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(deleteBtn.isEnabled())
+ deleteBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ deleteBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ downloadBtn = new JButton();
+ downloadBtn.setBorderPainted(false);
+ downloadBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border);
+ downloadBtn.setToolTipText("Download");
+ try {
+ downloadBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("download_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ downloadBtn.setBorderPainted(false);
+ downloadBtn.setEnabled(false);
+ }catch(IOException ex) {
+ downloadBtn.setText("Download");
+ }
+ downloadBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ JFileChooser fileChooser = new JFileChooser();
+ fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
+ int choiceFileChooser = fileChooser.showDialog(Desktop.this, "Save here");
+ if (choiceFileChooser == JFileChooser.APPROVE_OPTION) {
+ controller.downloadSelectedNodes(fileChooser.getSelectedFile().toString());
+ }
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(downloadBtn.isEnabled())
+ downloadBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ downloadBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ JButton uploadBtn = new JButton();
+ uploadBtn.setBorderPainted(false);
+ uploadBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border);
+ uploadBtn.setToolTipText("Upload here");
+ try {
+ uploadBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("upload_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ uploadBtn.setBackground(GuifyColors.GRAY);
+ uploadBtn.setBorderPainted(false);
+ uploadBtn.setEnabled(true);
+ }catch(IOException ex) {
+ uploadBtn.setText("Upload here");
+ }
+ uploadBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ JFileChooser fileChooser = new JFileChooser();
+ fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+ fileChooser.setMultiSelectionEnabled(true);
+ int choiceFileChooser = fileChooser.showDialog(Desktop.this, "Upload");
+ if (choiceFileChooser == JFileChooser.APPROVE_OPTION && fileChooser.getSelectedFiles().length > 0) {
+ try {
+ controller.uploadToRemoteServer(fileChooser.getSelectedFiles());
+ } catch (SftpException e1) {
+ if (e1.getMessage().contains("Permission denied")) {
+ JOptionPane.showMessageDialog(new JFrame(), "Not enough permissions to upload in this location", "Permission denied", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+ }
+ drawComponentsForDirectory(controller.getCurrentWorkingDirectory());
+ }
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(uploadBtn.isEnabled())
+ uploadBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ uploadBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ JButton queueBtn = new JButton();
+ queueBtn.setBorderPainted(false);
+ queueBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border);
+ queueBtn.setToolTipText("Queue");
+ try {
+ queueBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("queue_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ queueBtn.setBackground(GuifyColors.GRAY);
+ queueBtn.setBorderPainted(false);
+ queueBtn.setEnabled(true);
+ }catch(IOException ex) {
+ queueBtn.setText("Queue");
+ }
+ queueBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ QueueController queueController = new QueueController();
+ queueController.showFrame(true);
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(queueBtn.isEnabled())
+ queueBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ queueBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+
+ pathTextBox = new JTextField();
+ pathTextBox.setPreferredSize(new Dimension(100, pathTextBox.getPreferredSize().height));
+ Font font = pathTextBox.getFont();
+ Font biggerFont = font.deriveFont(font.getSize() + 4f); // Increase font size by 4
+ pathTextBox.setFont(biggerFont);
+ pathTextBox.addFocusListener(new FocusListener() {
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ if(pathTextBox != null && !pathTextBox.getText().equals(controller.getCurrentWorkingDirectory())) {
+ drawComponentsForDirectory(pathTextBox.getText());
+ }
+ }
+
+ @Override
+ public void focusGained(FocusEvent e) {}
+ });
+
+
+ JButton goToBtn = new JButton();
+ goToBtn.setBorderPainted(false);
+ goToBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border);
+ goToBtn.setToolTipText("Go");
+ try {
+ goToBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("go_to_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ goToBtn.setBackground(GuifyColors.GRAY);
+ goToBtn.setBorderPainted(false);
+ goToBtn.setEnabled(true);
+ }catch(IOException ex) {
+ goToBtn.setText("Go");
+ }
+ goToBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if(pathTextBox != null && !pathTextBox.getText().equals(controller.getCurrentWorkingDirectory())) {
+ drawComponentsForDirectory(pathTextBox.getText());
+ }
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(uploadBtn.isEnabled())
+ goToBtn.setBackground(GuifyColors.GRAY_HOVER);
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ goToBtn.setBackground(GuifyColors.GRAY);
+ }
+ });
+
+ toolBar.add(backBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(cutBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(copyBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(pasteBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(renameBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(newBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(deleteBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(downloadBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(uploadBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(queueBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(pathTextBox);
+ toolBar.add(Box.createHorizontalStrut(5));
+ toolBar.add(goToBtn);
+
+ }
+
+ /**
+ * Enables or disables tool bar buttons
+ * according to these propositions:
+ *
+ * 1. At least a selected node is selected <--> cut, copy, delete, download ENABLED
+ * 2. Only one selected node is selected <--> rename ENABLED
+ * 3. selectedToolBarOperation is not none <--> paste ENABLED
+ */
+ private void updateToolBarItems() {
+
+ int selectedNodes = controller.countSelectedNodes();
+ int selectedToolBarOperation = controller.cutCopyPasteController.getSelectedOperation();
+
+ if(selectedToolBarOperation != Constants.Constants_FSOperations.NONE) {
+ pasteBtn.setEnabled(true);
+ }
+ else {
+ pasteBtn.setEnabled(false);
+ }
+
+ if(selectedNodes == 1) {
+ renameBtn.setEnabled(true);
+ }
+ else {
+ renameBtn.setEnabled(false);
+ }
+
+ if(selectedNodes > 0) {
+ cutBtn.setEnabled(true);
+ copyBtn.setEnabled(true);
+ deleteBtn.setEnabled(true);
+ downloadBtn.setEnabled(true);
+ }
+ else {
+ cutBtn.setEnabled(false);
+ copyBtn.setEnabled(false);
+ deleteBtn.setEnabled(false);
+ downloadBtn.setEnabled(false);
+ }
+ }
+
+ /*
+ * ========== END Frame Drawing ==========
+ */
+
+
+ /*
+ * ========== BEGIN Node selection/deselection ==========
+ */
+
+ private void selectNode(JDirectoryNodeButton node) {
+ controller.addSelectedNode(node);
+ node.setBackground(new Color(204,238,255));
+ updateToolBarItems();
+ }
+
+ private void unselectAllNodes() {
+ controller.clearSelectedNodes();
+ // TODO: gotta enhance this for
+ for(Component component: desktopPanel.getComponents()) {
+ if(component instanceof JDirectoryNodeButton) {
+ component.setBackground(new Color(255, 255, 255));
+ }
+ }
+ updateToolBarItems();
+ }
+
+ private void unselectNode(JDirectoryNodeButton sender) {
+ controller.removeSelectedNode(sender);
+ sender.setBackground(new Color(255, 255, 255));
+ updateToolBarItems();
+ }
+
+ /*
+ * ========== END Node selection/deselection ==========
+ */
+
+}
diff --git a/Guify/src/views/FindAndReplace.java b/Guify/src/views/FindAndReplace.java
new file mode 100644
index 0000000..732815e
--- /dev/null
+++ b/Guify/src/views/FindAndReplace.java
@@ -0,0 +1,105 @@
+package views;
+
+import java.awt.event.*;
+import javax.swing.*;
+
+import controllers.FindAndReplaceController;
+import views.interfaces.IFindAndReplaceFrame;
+
+// This JFrame has been forked from the project "Simple-Java-Text-Editor" you
+// can find at its link: https://github.com/pH-7/Simple-Java-Text-Editor.
+// Its license can be found here https://github.com/pH-7/Simple-Java-Text-Editor/blob/master/license.txt (Apache License 2.0)
+
+public class FindAndReplace extends JFrame implements IFindAndReplaceFrame {
+
+ private static final long serialVersionUID = 1L;
+
+ public FindAndReplace(Object controller) {
+ JLabel lab1 = new JLabel("Find:");
+ JLabel lab2 = new JLabel("Replace:");
+ JTextField findTextField = new JTextField(30);
+ JTextField replaceTextField = new JTextField(30);
+ JButton findNextBtn = new JButton("Find Next");
+ findNextBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e){
+ ((FindAndReplaceController) controller).findNext(findTextField.getText());
+ }
+ });
+
+ JButton findPreviousBtn = new JButton("Find Previous");
+ findPreviousBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e){
+ ((FindAndReplaceController) controller).findPrevious(findTextField.getText());
+ }
+ });
+
+ JButton replaceNextBtn = new JButton("Replace Next");
+ replaceNextBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e){
+ ((FindAndReplaceController) controller).replaceNext(findTextField.getText(), replaceTextField.getText());
+ }
+ });
+
+ JButton replaceAllBtn = new JButton("Replace All");
+ replaceAllBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e){
+ ((FindAndReplaceController) controller).replaceAll(findTextField.getText(), replaceTextField.getText());
+ }
+ });
+
+ JButton cancel = new JButton("Cancel");
+ cancel.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e){
+ dispose();
+ }
+ });
+
+ setLayout(null);
+
+ // Set the width and height of the label
+ int labWidth = 80;
+ int labHeight = 20;
+
+ // Adding labels
+ lab1.setBounds(10,10, labWidth, labHeight);
+ add(lab1);
+ findTextField.setBounds(10+labWidth, 10, 120, 20);
+ add(findTextField);
+ lab2.setBounds(10, 10+labHeight+10, labWidth, labHeight);
+ add(lab2);
+ replaceTextField.setBounds(10+labWidth, 10+labHeight+10, 120, 20);
+ add(replaceTextField);
+
+ // Adding buttons
+ findNextBtn.setBounds(225, 6, 115, 20);
+ add(findNextBtn);
+
+ findPreviousBtn.setBounds(225, 28, 115, 20);
+ add(findPreviousBtn);
+
+ replaceNextBtn.setBounds(225, 50, 115, 20);
+ add(replaceNextBtn);
+
+ replaceAllBtn.setBounds(225, 72, 115, 20);
+ add(replaceAllBtn);
+
+ cancel.setBounds(225, 94, 115, 20);
+ add(cancel);
+
+ // Set the width and height of the window
+ int width = 360;
+ int height = 160;
+
+ // Set size window
+ setSize(width, height);
+ setResizable(false);
+ setTitle("Find/Replace");
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ setAlwaysOnTop(true);
+ }
+}
diff --git a/Guify/src/views/Login.java b/Guify/src/views/Login.java
new file mode 100644
index 0000000..5a836f9
--- /dev/null
+++ b/Guify/src/views/Login.java
@@ -0,0 +1,124 @@
+package views;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.border.EmptyBorder;
+
+import code.Constants;
+import code.Constants.GuifyColors;
+import controllers.LoginController;
+import views.interfaces.ILoginFrame;
+
+import javax.swing.JPasswordField;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JTextField;
+import javax.swing.JButton;
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.awt.Color;
+
+public class Login extends JFrame implements ILoginFrame {
+
+ private static final long serialVersionUID = 1;
+ private LoginController controller;
+ private JPanel contentPane;
+ private JPasswordField passwordField;
+ private JTextField usernameField;
+ private JTextField hostField;
+ private JTextField portField;
+
+ public Login(Object controller) {
+ this.controller = (LoginController) controller;
+
+ setTitle(Constants.APP_NAME);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setBounds(100, 100, 312, 400);
+ contentPane = new JPanel();
+ contentPane.setForeground(Color.WHITE);
+ contentPane.setBackground(Color.WHITE);
+ contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
+
+ setContentPane(contentPane);
+ contentPane.setLayout(null);
+
+ passwordField = new JPasswordField();
+ passwordField.setToolTipText("SSH Password");
+ passwordField.setBounds(10, 159, 139, 20);
+ contentPane.add(passwordField);
+
+ JLabel lblNewLabel = new JLabel("Username");
+ lblNewLabel.setBounds(10, 78, 139, 14);
+ contentPane.add(lblNewLabel);
+
+ JLabel lblNewLabel_1 = new JLabel("Password");
+ lblNewLabel_1.setBounds(10, 134, 139, 14);
+ contentPane.add(lblNewLabel_1);
+
+ usernameField = new JTextField();
+ usernameField.setToolTipText("SSH Username");
+ usernameField.setBounds(10, 103, 139, 20);
+ contentPane.add(usernameField);
+ usernameField.setColumns(10);
+
+ JButton btnConnect = new JButton("Connect");
+ btnConnect.setForeground(Color.WHITE);
+ btnConnect.setBackground(GuifyColors.BLUE);
+ btnConnect.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ btnConnect_OnClick();
+ }
+ });
+ btnConnect.setBounds(75, 297, 139, 30);
+ contentPane.add(btnConnect);
+
+ JLabel lblNewLabel_2 = new JLabel("Host");
+ lblNewLabel_2.setBounds(10, 22, 139, 14);
+ contentPane.add(lblNewLabel_2);
+
+ hostField = new JTextField();
+ hostField.setToolTipText("SSH Host");
+ hostField.setBounds(10, 47, 139, 20);
+ contentPane.add(hostField);
+ hostField.setColumns(10);
+
+ JLabel lblNewLabel_3 = new JLabel("Port");
+ lblNewLabel_3.setBounds(10, 190, 139, 14);
+ contentPane.add(lblNewLabel_3);
+
+ portField = new JTextField();
+ portField.setText("22");
+ portField.setToolTipText("SSH Port");
+ portField.setBounds(10, 215, 86, 20);
+ contentPane.add(portField);
+ portField.setColumns(10);
+
+
+ }
+
+ /**
+ * Events
+ */
+ private void btnConnect_OnClick() {
+
+ String host = hostField.getText();
+ String username = usernameField.getText();
+ String password = String.valueOf(passwordField.getPassword());
+ String port = portField.getText();
+
+ // Perform validation
+ try {
+ controller.ValidateInput(host, username, password, port);
+ }
+ catch(IllegalArgumentException ex) {
+ JOptionPane.showMessageDialog(new JFrame(), ex.getMessage(), "Attention required", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ // Perform login
+ if(!controller.Login(host, username, password, port)) {
+ JOptionPane.showMessageDialog(new JFrame(), "SSH Login failed", "SSH Login failed", JOptionPane.ERROR_MESSAGE);
+ }
+ }
+
+}
diff --git a/Guify/src/views/Notepad.java b/Guify/src/views/Notepad.java
new file mode 100644
index 0000000..62ebf1a
--- /dev/null
+++ b/Guify/src/views/Notepad.java
@@ -0,0 +1,173 @@
+package views;
+
+import java.awt.Color;
+import javax.imageio.ImageIO;
+import javax.swing.Box;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JTextArea;
+import javax.swing.JToolBar;
+import javax.swing.border.EmptyBorder;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import code.GuiAbstractions.Implementations.JGenericTextArea;
+import controllers.NotepadController;
+import views.interfaces.INotepadFrame;
+
+import java.awt.Font;
+import java.awt.Image;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.IOException;
+import javax.swing.JScrollPane;
+public class Notepad extends JFrame implements INotepadFrame {
+
+ private static final long serialVersionUID = 1L;
+ private NotepadController controller;
+ /**
+ * Create the application.
+ */
+
+ JTextArea textArea;
+
+ public Notepad(Object controller) {
+ this.controller = (NotepadController) controller;
+ setTitle(this.controller.getFilePath());
+ setBounds(100, 100, 800, 600);
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ getContentPane().setLayout(null);
+
+ JScrollPane scrollPane = new JScrollPane();
+ scrollPane.setBounds(10, 38, 764, 512);
+ getContentPane().add(scrollPane);
+
+ textArea = new JTextArea();
+ scrollPane.setViewportView(textArea);
+ textArea.setTabSize(4);
+ textArea.setFont(new Font("Monospaced", Font.PLAIN, 14));
+ textArea.setCaretPosition(0);
+ textArea.getDocument().addDocumentListener(new DocumentListener() {
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ // Called when text is inserted into the document
+ handleTextChange();
+ }
+
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ // Called when text is removed from the document
+ handleTextChange();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ // Called when attributes of the document change
+ // NO OP
+ }
+
+ private void handleTextChange() {
+ if(!((NotepadController)controller).isUnsaved()) {
+ ((NotepadController)controller).setUnsaved(true);
+ setTitle(((NotepadController)controller).getTitle());
+ }
+ }
+ });
+
+ JToolBar toolBar = new JToolBar();
+ toolBar.setFloatable(false);
+ toolBar.setBounds(10, 0, 614, 37);
+ toolBar.setBackground(new Color(240, 240, 240));
+
+ JButton saveBtn = new JButton();
+ saveBtn.setBorderPainted(false);
+ saveBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border
+ saveBtn.setToolTipText("Save");
+ try {
+ saveBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("save_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ saveBtn.setBackground(new Color(240, 240, 240));
+ saveBtn.setBorderPainted(false);
+ saveBtn.setEnabled(true);
+ }catch(IOException ex) {
+ saveBtn.setText("Save");
+ }
+ saveBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ ((NotepadController)controller).writeOnFile(textArea.getText());
+ ((NotepadController)controller).setUnsaved(false);
+ setTitle(((NotepadController)controller).getTitle());
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(saveBtn.isEnabled())
+ saveBtn.setBackground(new Color(220, 220, 220));
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ saveBtn.setBackground(new Color(240, 240, 240));
+ }
+ });
+
+ JButton searchBtn = new JButton();
+ searchBtn.setBorderPainted(false);
+ searchBtn.setBorder(new EmptyBorder(0, 0, 0, 0)); // Set empty border);
+ searchBtn.setToolTipText("Serch/Replace");
+ try {
+ searchBtn.setIcon(new ImageIcon(ImageIO.read(getClass().getClassLoader().getResource("search_icon.png")).getScaledInstance(25, 25, Image.SCALE_SMOOTH)));
+ searchBtn.setBackground(new Color(240, 240, 240));
+ searchBtn.setBorderPainted(false);
+ searchBtn.setEnabled(true);
+ }catch(IOException ex) {
+ searchBtn.setText("Serch/Replace");
+ }
+ searchBtn.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ ((NotepadController)controller).showFindAndReplace(new JGenericTextArea(textArea));
+ }
+
+ // Hover on
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ if(searchBtn.isEnabled())
+ searchBtn.setBackground(new Color(220, 220, 220));
+ }
+
+ // Hover off
+ @Override
+ public void mouseExited(MouseEvent e) {
+ searchBtn.setBackground(new Color(240, 240, 240));
+ }
+ });
+
+ toolBar.add(saveBtn);
+ toolBar.add(Box.createHorizontalStrut(15));
+ toolBar.add(searchBtn);
+
+ getContentPane().add(toolBar);
+
+ /**
+ * Close "Find and Replace" if this window
+ * gets closed
+ */
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ ((NotepadController)controller).disposeFindAndReplaceFrame();
+ }
+ });
+ }
+
+ public void displayContent(String content) {
+ textArea.setText(content);
+ }
+
+}
diff --git a/Guify/src/views/Queue.java b/Guify/src/views/Queue.java
new file mode 100644
index 0000000..d795fa2
--- /dev/null
+++ b/Guify/src/views/Queue.java
@@ -0,0 +1,84 @@
+package views;
+
+import javax.swing.JFrame;
+import javax.swing.JProgressBar;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import javax.swing.JTable;
+import javax.swing.JScrollPane;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableColumn;
+
+import code.Constants;
+import views.interfaces.IQueueFrame;
+public class Queue extends JFrame implements IQueueFrame {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ *
+ * Custom cell renderer in order to be able to display
+ * a progress bar in the JTable
+ *
+ */
+ public static class ProgressBarTableCellRenderer extends DefaultTableCellRenderer {
+ private static final long serialVersionUID = 1L;
+ private JProgressBar progressBar;
+
+ public ProgressBarTableCellRenderer() {
+ super();
+ progressBar = new JProgressBar();
+ progressBar.setStringPainted(true);
+ }
+
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ if (value instanceof Integer) {
+ int progressValue = (Integer) value;
+ progressBar.setValue(progressValue);
+ progressBar.setString(progressValue + "%");
+ }
+
+ progressBar.setStringPainted(true);
+ progressBar.setForeground(Constants.GuifyColors.BLUE);
+ progressBar.setBackground(Color.WHITE);
+
+ return progressBar;
+ }
+ }
+
+ public DefaultTableModel tableModel;
+
+ public Queue() {
+ setTitle("Queue");
+ String[] columnNames = {"Source", "Destination", "Operation", "Percentage"};
+ tableModel = new DefaultTableModel(columnNames, 0);
+ JTable table = new JTable(tableModel);
+ table.setEnabled(false); // Prevents user editing
+ // Show percentage by using a custom cell renderer
+ TableColumn percentageColumn = table.getColumnModel().getColumn(3);
+ percentageColumn.setCellRenderer(new ProgressBarTableCellRenderer());
+ JScrollPane scrollPane = new JScrollPane(table);
+ this.getContentPane().add(scrollPane, BorderLayout.CENTER);
+ this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ this.pack();
+ }
+
+ /**
+ * Adds a row in the JTable
+ * @return The index of the inserted row
+ */
+ public int addRow(String source, String destination, String operation, int percentage) {
+ tableModel.addRow(new Object[]{source, destination, operation, percentage});
+ return tableModel.getRowCount() - 1;
+ }
+
+ public void updateRow(int rowIndex, int percentage) {
+ if(rowIndex < tableModel.getRowCount()) {
+ tableModel.setValueAt(percentage, rowIndex, 3);
+ }
+ }
+}
diff --git a/Guify/src/views/interfaces/IDesktopFrame.java b/Guify/src/views/interfaces/IDesktopFrame.java
new file mode 100644
index 0000000..532b3da
--- /dev/null
+++ b/Guify/src/views/interfaces/IDesktopFrame.java
@@ -0,0 +1,6 @@
+package views.interfaces;
+
+public interface IDesktopFrame {
+ void drawComponentsForDirectory(String directory);
+ void setVisible(boolean visible);
+}
diff --git a/Guify/src/views/interfaces/IFindAndReplaceFrame.java b/Guify/src/views/interfaces/IFindAndReplaceFrame.java
new file mode 100644
index 0000000..56d831a
--- /dev/null
+++ b/Guify/src/views/interfaces/IFindAndReplaceFrame.java
@@ -0,0 +1,9 @@
+package views.interfaces;
+
+public interface IFindAndReplaceFrame {
+ int getWidth();
+ int getHeight();
+ void setLocation(int x, int y);
+ void setVisible(boolean visible);
+ void dispose();
+}
diff --git a/Guify/src/views/interfaces/ILoginFrame.java b/Guify/src/views/interfaces/ILoginFrame.java
new file mode 100644
index 0000000..7f5f60c
--- /dev/null
+++ b/Guify/src/views/interfaces/ILoginFrame.java
@@ -0,0 +1,5 @@
+package views.interfaces;
+
+public interface ILoginFrame {
+ void setVisible(boolean visible);
+}
diff --git a/Guify/src/views/interfaces/INotepadFrame.java b/Guify/src/views/interfaces/INotepadFrame.java
new file mode 100644
index 0000000..2bea330
--- /dev/null
+++ b/Guify/src/views/interfaces/INotepadFrame.java
@@ -0,0 +1,10 @@
+package views.interfaces;
+
+public interface INotepadFrame {
+ void setVisible(boolean visible);
+ void displayContent(String content);
+ int getX();
+ int getY();
+ int getWidth();
+ int getHeight();
+}
diff --git a/Guify/src/views/interfaces/IQueueFrame.java b/Guify/src/views/interfaces/IQueueFrame.java
new file mode 100644
index 0000000..4ba8144
--- /dev/null
+++ b/Guify/src/views/interfaces/IQueueFrame.java
@@ -0,0 +1,7 @@
+package views.interfaces;
+
+public interface IQueueFrame {
+ public void setVisible(boolean visible);
+ int addRow(String source, String destination, String operation, int percentage);
+ void updateRow(int rowIndex, int percentage);
+}
diff --git a/Images/Image.jpg b/Images/Image.jpg
new file mode 100644
index 0000000..7b711e1
Binary files /dev/null and b/Images/Image.jpg differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a4e2a89
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+# Guify
+Guify creates a Graphical User Interface for SSH.
+
+Works on any machine able to execute Java code, but is able to target only Linux (or more in general POSIX-compliant) systems.
+
+## Features:
+- Navigation in the File System;
+- Common file operations (cut, copy, rename, delete, paste, create);
+- File and folder transfer (to/from);
+- Integrated file editor.
+
+## Alternatives
+If you want to achieve similar results you can:
+* Use [WinSCP](https://winscp.net/eng/index.php) (Windows only);
+* Use a file manager like Nautilus, Konqueror, PCManFM (Linux examples)
+* Configure an FTP server and accessing it through FileZilla;
+* Mount a remote partition locally with the SMB protocol;
+* Route X11 output to your local machine over SSH;
+* ...
+
+## Screenshots
+
+