diff --git a/gui/griffon-app/.DS_Store b/gui/griffon-app/.DS_Store new file mode 100644 index 00000000..db3318cb Binary files /dev/null and b/gui/griffon-app/.DS_Store differ diff --git a/gui/griffon-app/controllers/com/muwire/gui/AddCommentController.groovy b/gui/griffon-app/controllers/com/muwire/gui/AddCommentController.groovy index 3f5050be..0b618b2c 100644 --- a/gui/griffon-app/controllers/com/muwire/gui/AddCommentController.groovy +++ b/gui/griffon-app/controllers/com/muwire/gui/AddCommentController.groovy @@ -27,7 +27,7 @@ class AddCommentController { model.selectedFiles.each { it.setComment(comment) } - mvcGroup.parentGroup.view.builder.getVariable("shared-files-table").model.fireTableDataChanged() + mvcGroup.parentGroup.view.refreshSharedFiles() cancel() } diff --git a/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy b/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy index 1dbba07e..1af27669 100644 --- a/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy +++ b/gui/griffon-app/controllers/com/muwire/gui/OptionsController.groovy @@ -143,6 +143,8 @@ class OptionsController { // boolean showSearchHashes = view.showSearchHashesCheckbox.model.isSelected() // model.showSearchHashes = showSearchHashes // uiSettings.showSearchHashes = showSearchHashes + + uiSettings.sharedFilesAsTree = model.sharedFilesAsTree File uiSettingsFile = new File(core.home, "gui.properties") uiSettingsFile.withOutputStream { @@ -168,4 +170,14 @@ class OptionsController { if (rv == JFileChooser.APPROVE_OPTION) model.downloadLocation = chooser.getSelectedFile().getAbsolutePath() } + + @ControllerAction + void sharedTree() { + model.sharedFilesAsTree = true + } + + @ControllerAction + void sharedTable() { + model.sharedFilesAsTree = false + } } \ No newline at end of file diff --git a/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy b/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy index 0cac1ccd..7587f979 100644 --- a/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/MainFrameModel.groovy @@ -1,6 +1,7 @@ package com.muwire.gui import java.util.concurrent.ConcurrentHashMap +import java.nio.file.Path import java.util.Calendar import java.util.UUID @@ -8,12 +9,16 @@ import javax.annotation.Nonnull import javax.inject.Inject import javax.swing.JOptionPane import javax.swing.JTable +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel +import javax.swing.tree.TreeNode import com.muwire.core.Core import com.muwire.core.InfoHash import com.muwire.core.MuWireSettings import com.muwire.core.Persona import com.muwire.core.RouterDisconnectedEvent +import com.muwire.core.SharedFile import com.muwire.core.connection.ConnectionAttemptStatus import com.muwire.core.connection.ConnectionEvent import com.muwire.core.connection.DisconnectionEvent @@ -57,6 +62,8 @@ class MainFrameModel { FactoryBuilderSupport builder @MVCMember @Nonnull MainFrameController controller + @MVCMember @Nonnull + MainFrameView view @Inject @Nonnull GriffonApplication application @Observable boolean coreInitialized = false @Observable boolean routerPresent @@ -64,7 +71,10 @@ class MainFrameModel { def results = new ConcurrentHashMap<>() def downloads = [] def uploads = [] - def shared = [] + def shared + def sharedTree + def treeRoot + final Map fileToNode = new HashMap<>() def watched = [] def connectionList = [] def searches = new LinkedList() @@ -122,6 +132,13 @@ class MainFrameModel { void mvcGroupInit(Map args) { uiSettings = application.context.get("ui-settings") + + if (!uiSettings.sharedFilesAsTree) + shared = [] + else { + treeRoot = new DefaultMutableTreeNode() + sharedTree = new DefaultTreeModel(treeRoot) + } Timer timer = new Timer("download-pumper", true) timer.schedule({ @@ -303,7 +320,6 @@ class MainFrameModel { void onFileHashingEvent(FileHashingEvent e) { runInsideUIAsync { - loadedFiles = shared.size() hashingFile = e.hashingFile } } @@ -315,28 +331,53 @@ class MainFrameModel { if (e.error != null) return // TODO do something runInsideUIAsync { - shared << e.sharedFile - loadedFiles = shared.size() - JTable table = builder.getVariable("shared-files-table") - table.model.fireTableDataChanged() + if (!uiSettings.sharedFilesAsTree) { + shared << e.sharedFile + loadedFiles = shared.size() + JTable table = builder.getVariable("shared-files-table") + table.model.fireTableDataChanged() + } else { + insertIntoTree(e.sharedFile) + } } } void onFileLoadedEvent(FileLoadedEvent e) { runInsideUIAsync { - shared << e.loadedFile - loadedFiles = shared.size() - JTable table = builder.getVariable("shared-files-table") - table.model.fireTableDataChanged() + if (!uiSettings.sharedFilesAsTree) { + shared << e.loadedFile + loadedFiles = shared.size() + JTable table = builder.getVariable("shared-files-table") + table.model.fireTableDataChanged() + } else { + insertIntoTree(e.loadedFile) + } } } void onFileUnsharedEvent(FileUnsharedEvent e) { runInsideUIAsync { - shared.remove(e.unsharedFile) - loadedFiles = shared.size() - JTable table = builder.getVariable("shared-files-table") - table.model.fireTableDataChanged() + if (!uiSettings.sharedFilesAsTree) { + shared.remove(e.unsharedFile) + loadedFiles = shared.size() + } else { + def dmtn = fileToNode.remove(e.unsharedFile) + if (dmtn != null) { + loadedFiles = fileToNode.size() + while (true) { + def parent = dmtn.getParent() + parent.remove(dmtn) + if (parent == treeRoot) + break + if (parent.getChildCount() == 0) { + dmtn = parent + continue + } + break + } + } + } + view.refreshSharedFiles() } } @@ -476,11 +517,44 @@ class MainFrameModel { if (!core.muOptions.shareDownloadedFiles) return runInsideUIAsync { - shared << e.downloadedFile - JTable table = builder.getVariable("shared-files-table") - table.model.fireTableDataChanged() + if (!uiSettings.sharedFilesAsTree) { + shared << e.downloadedFile + JTable table = builder.getVariable("shared-files-table") + table.model.fireTableDataChanged() + } else { + insertIntoTree(e.downloadedFile) + } } } + + private void insertIntoTree(SharedFile file) { + Path folder = file.getFile().toPath() + folder = folder.subpath(0, folder.getNameCount() - 1) + TreeNode node = treeRoot + for(Path path : folder) { + boolean exists = false + def children = node.children() + def child = null + while(children.hasMoreElements()) { + child = children.nextElement() + if (child.getUserObject() == path.toString()) { + exists = true + break + } + } + if (!exists) { + child = new DefaultMutableTreeNode(path.toString()) + node.add(child) + } + node = child + } + + def dmtn = new DefaultMutableTreeNode(file) + fileToNode[file] = dmtn + node.add(dmtn) + loadedFiles = fileToNode.size() + view.refreshSharedFiles() + } private static class UIConnection { Destination destination diff --git a/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy b/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy index 634b2511..968acb48 100644 --- a/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy +++ b/gui/griffon-app/models/com/muwire/gui/OptionsModel.groovy @@ -31,6 +31,7 @@ class OptionsModel { @Observable boolean clearFinishedDownloads @Observable boolean excludeLocalResult @Observable boolean showSearchHashes + @Observable boolean sharedFilesAsTree // bw options @Observable String inBw @@ -67,6 +68,7 @@ class OptionsModel { clearFinishedDownloads = uiSettings.clearFinishedDownloads excludeLocalResult = uiSettings.excludeLocalResult showSearchHashes = uiSettings.showSearchHashes + sharedFilesAsTree = uiSettings.sharedFilesAsTree if (core.router != null) { inBw = String.valueOf(settings.inBw) diff --git a/gui/griffon-app/resources/comment.png b/gui/griffon-app/resources/comment.png new file mode 100644 index 00000000..c5fc0255 Binary files /dev/null and b/gui/griffon-app/resources/comment.png differ diff --git a/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy b/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy index abe7f090..03fed4d7 100644 --- a/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/MainFrameView.groovy @@ -16,11 +16,14 @@ import javax.swing.JMenuItem import javax.swing.JPopupMenu import javax.swing.JSplitPane import javax.swing.JTable +import javax.swing.JTree import javax.swing.ListSelectionModel import javax.swing.SwingConstants import javax.swing.TransferHandler import javax.swing.border.Border import javax.swing.table.DefaultTableCellRenderer +import javax.swing.tree.TreeNode +import javax.swing.tree.TreePath import com.muwire.core.Constants import com.muwire.core.MuWireSettings @@ -59,8 +62,10 @@ class MainFrameView { def lastWatchedSortEvent def trustTablesSortEvents = [:] + UISettings settings + void initUI() { - UISettings settings = application.context.get("ui-settings") + settings = application.context.get("ui-settings") builder.with { application(size : [1024,768], id: 'main-frame', locationRelativeTo : null, @@ -207,12 +212,18 @@ class MainFrameView { panel { borderLayout() scrollPane(constraints : BorderLayout.CENTER) { - table(id : "shared-files-table", autoCreateRowSorter: true) { - tableModel(list : model.shared) { - closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()}) - closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() }) - closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null}) + if (!settings.sharedFilesAsTree) { + table(id : "shared-files-table", autoCreateRowSorter: true) { + tableModel(list : model.shared) { + closureColumn(header : "Name", preferredWidth : 500, type : String, read : {row -> row.getCachedPath()}) + closureColumn(header : "Size", preferredWidth : 100, type : Long, read : {row -> row.getCachedLength() }) + closureColumn(header : "Comments", preferredWidth : 100, type : Boolean, read : {it.getComment() != null}) + } } + } else { + def jtree = new JTree(model.sharedTree) + jtree.setCellRenderer(new SharedTreeRenderer()) + tree(id : "shared-files-tree", rootVisible : false, jtree) } } } @@ -227,7 +238,7 @@ class MainFrameView { gridLayout(rows : 1, cols : 2) panel { label("Shared:") - label(text : bind {model.loadedFiles.toString()}) + label(text : bind {model.loadedFiles}, id : "shared-files-count") } panel { button(text : "Add Comment", enabled : bind {model.addCommentButtonEnabled}, addCommentAction) @@ -489,13 +500,7 @@ class MainFrameView { } }) - // shared files table - def sharedFilesTable = builder.getVariable("shared-files-table") - sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer()) - - sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt}) - sharedFilesTable.rowSorter.setSortsOnUpdates(true) - + // shared files menu JPopupMenu sharedFilesMenu = new JPopupMenu() JMenuItem copyHashToClipboard = new JMenuItem("Copy hash to clipboard") copyHashToClipboard.addActionListener({mvcGroup.view.copyHashToClipboard()}) @@ -506,7 +511,8 @@ class MainFrameView { JMenuItem commentSelectedFiles = new JMenuItem("Comment selected files") commentSelectedFiles.addActionListener({mvcGroup.controller.addComment()}) sharedFilesMenu.add(commentSelectedFiles) - sharedFilesTable.addMouseListener(new MouseAdapter() { + + def sharedFilesMouseListener = new MouseAdapter() { @Override public void mouseReleased(MouseEvent e) { if (e.isPopupTrigger()) @@ -517,15 +523,36 @@ class MainFrameView { if (e.isPopupTrigger()) showPopupMenu(sharedFilesMenu, e) } - }) + } - selectionModel = sharedFilesTable.getSelectionModel() - selectionModel.addListSelectionListener({ - def selectedFiles = selectedSharedFiles() - if (selectedFiles == null || selectedFiles.isEmpty()) - return - model.addCommentButtonEnabled = true - }) + // shared files table or tree + if (!settings.sharedFilesAsTree) { + def sharedFilesTable = builder.getVariable("shared-files-table") + sharedFilesTable.columnModel.getColumn(1).setCellRenderer(new SizeRenderer()) + + sharedFilesTable.rowSorter.addRowSorterListener({evt -> lastSharedSortEvent = evt}) + sharedFilesTable.rowSorter.setSortsOnUpdates(true) + + sharedFilesTable.addMouseListener(sharedFilesMouseListener) + + selectionModel = sharedFilesTable.getSelectionModel() + selectionModel.addListSelectionListener({ + def selectedFiles = selectedSharedFiles() + if (selectedFiles == null || selectedFiles.isEmpty()) + return + model.addCommentButtonEnabled = true + }) + } else { + def sharedFilesTree = builder.getVariable("shared-files-tree") + sharedFilesTree.addMouseListener(sharedFilesMouseListener) + + sharedFilesTree.addTreeSelectionListener({ + def selectedNode = sharedFilesTree.getLastSelectedPathComponent() + model.addCommentButtonEnabled = selectedNode != null + + }) + // TODO: other stuff + } // searches table def searchesTable = builder.getVariable("searches-table") @@ -656,20 +683,40 @@ class MainFrameView { } def selectedSharedFiles() { - def sharedFilesTable = builder.getVariable("shared-files-table") - int[] selected = sharedFilesTable.getSelectedRows() - if (selected.length == 0) - return null - List rv = new ArrayList<>() - if (lastSharedSortEvent != null) { - for (int i = 0; i < selected.length; i ++) { - selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i]) + if (!settings.sharedFilesAsTree) { + def sharedFilesTable = builder.getVariable("shared-files-table") + int[] selected = sharedFilesTable.getSelectedRows() + if (selected.length == 0) + return null + List rv = new ArrayList<>() + if (lastSharedSortEvent != null) { + for (int i = 0; i < selected.length; i ++) { + selected[i] = sharedFilesTable.rowSorter.convertRowIndexToModel(selected[i]) + } } + selected.each { + rv.add(model.shared[it]) + } + return rv + } else { + def sharedFilesTree = builder.getVariable("shared-files-tree") + List rv = new ArrayList<>() + for (TreePath path : sharedFilesTree.getSelectionPaths()) { + getLeafs(path.getLastPathComponent(), rv) + } + return rv } - selected.each { - rv.add(model.shared[it]) + } + + private static void getLeafs(TreeNode node, List dest) { + if (node.isLeaf()) { + dest.add(node.getUserObject()) + return + } + def children = node.children() + while(children.hasMoreElements()) { + getLeafs(children.nextElement(), dest) } - rv } def copyHashToClipboard() { @@ -876,4 +923,12 @@ class MainFrameView { selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow) selectedRow } + + public void refreshSharedFiles() { + if (settings.sharedFilesAsTree) { + model.sharedTree.nodeStructureChanged(model.treeRoot) + } else { + builder.getVariable("shared-files-table").model.fireTableDataChanged() + } + } } \ No newline at end of file diff --git a/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy b/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy index 8c882b63..40031698 100644 --- a/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/OptionsView.groovy @@ -126,6 +126,12 @@ class OptionsView { excludeLocalResultCheckbox = checkBox(selected : bind {model.excludeLocalResult}, constraints : gbc(gridx: 1, gridy : 6)) // label(text : "Show Hash Searches In Monitor", constraints: gbc(gridx:0, gridy:7)) // showSearchHashesCheckbox = checkBox(selected : bind {model.showSearchHashes}, constraints : gbc(gridx: 1, gridy: 7)) + label(text : "Show Shared Files as", constraints: gbc(gridx: 0, gridy:8)) + panel( constraints : gbc(gridx: 1, gridy: 8)) { + buttonGroup(id : "viewShared") + radioButton(text: "Tree", selected : bind {model.sharedFilesAsTree}, buttonGroup: viewShared, sharedTreeAction) + radioButton(text: "Table", selected : bind {!model.sharedFilesAsTree}, buttonGroup: viewShared, sharedTableAction) + } } bandwidth = builder.panel { gridBagLayout() diff --git a/gui/src/main/groovy/com/muwire/gui/SharedTreeRenderer.groovy b/gui/src/main/groovy/com/muwire/gui/SharedTreeRenderer.groovy new file mode 100644 index 00000000..0e3622f8 --- /dev/null +++ b/gui/src/main/groovy/com/muwire/gui/SharedTreeRenderer.groovy @@ -0,0 +1,44 @@ +package com.muwire.gui + +import java.awt.Component + +import javax.swing.ImageIcon +import javax.swing.JTree +import javax.swing.tree.DefaultTreeCellRenderer + +import com.muwire.core.SharedFile + +import net.i2p.data.DataHelper + +class SharedTreeRenderer extends DefaultTreeCellRenderer { + + private final ImageIcon commentIcon + + SharedTreeRenderer() { + commentIcon = new ImageIcon((URL) SharedTreeRenderer.class.getResource("/comment.png")) + } + + public Component getTreeCellRendererComponent(JTree tree, Object value, + boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { + + def userObject = value.getUserObject() + def defaultRenderer = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus) + if (userObject instanceof String || userObject == null) + return defaultRenderer + + + SharedFile sf = (SharedFile) userObject + String name = sf.getFile().getName() + long length = sf.getCachedLength() + String formatted = DataHelper.formatSize2Decimal(length, false)+"B" + + + setText("$name ($formatted)") + setEnabled(true) + if (sf.comment != null) { + setIcon(commentIcon) + } + + this + } +} diff --git a/gui/src/main/groovy/com/muwire/gui/UISettings.groovy b/gui/src/main/groovy/com/muwire/gui/UISettings.groovy index f4bc0fab..b29d838b 100644 --- a/gui/src/main/groovy/com/muwire/gui/UISettings.groovy +++ b/gui/src/main/groovy/com/muwire/gui/UISettings.groovy @@ -9,6 +9,7 @@ class UISettings { boolean clearFinishedDownloads boolean excludeLocalResult boolean showSearchHashes + boolean sharedFilesAsTree UISettings(Properties props) { lnf = props.getProperty("lnf", "system") @@ -18,6 +19,7 @@ class UISettings { clearFinishedDownloads = Boolean.parseBoolean(props.getProperty("clearFinishedDownloads","false")) excludeLocalResult = Boolean.parseBoolean(props.getProperty("excludeLocalResult","true")) showSearchHashes = Boolean.parseBoolean(props.getProperty("showSearchHashes","true")) + sharedFilesAsTree = Boolean.parseBoolean(props.getProperty("sharedFilesAsTree","true")) } void write(OutputStream out) throws IOException { @@ -28,6 +30,7 @@ class UISettings { props.setProperty("clearFinishedDownloads", String.valueOf(clearFinishedDownloads)) props.setProperty("excludeLocalResult", String.valueOf(excludeLocalResult)) props.setProperty("showSearchHashes", String.valueOf(showSearchHashes)) + props.setProperty("sharedFilesAsTree", String.valueOf(sharedFilesAsTree)) if (font != null) props.setProperty("font", font)