From 5a9d821d83b3c0c60d5c4158ea6d4324494cf2f3 Mon Sep 17 00:00:00 2001 From: Louis Bergelson Date: Tue, 19 Nov 2024 18:21:22 -0500 Subject: [PATCH] Add Recent Files Menu and Improve Recent Sessions (#1616) * Refactoring of the load files menu option # Conflicts: # src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java * Made the list of recent sessions update dynamically as sessions are create/loaded * previously it only updated once when IGV was loaded * Adding a "Recent Files" menu option * This dynamically tracks the most recently loaded files/urls and allows reopening them * Fixes #https://github.com/igvteam/igv/issues/1547 * Add comments and MenuSelectedListener * Fix null check --- .../java/org/broad/igv/prefs/Constants.java | 1 + .../org/broad/igv/prefs/IGVPreferences.java | 26 ++- .../DynamicMenuItemsAdjustmentListener.java | 76 ++++++++ src/main/java/org/broad/igv/ui/IGV.java | 77 +++++--- .../java/org/broad/igv/ui/IGVMenuBar.java | 120 +++--------- .../broad/igv/ui/MenuSelectedListener.java | 19 ++ .../java/org/broad/igv/ui/RecentFileSet.java | 45 +++++ .../java/org/broad/igv/ui/RecentUrlsSet.java | 75 +++++++ src/main/java/org/broad/igv/ui/StackSet.java | 137 +++++++++++++ .../igv/ui/action/LoadFilesMenuAction.java | 46 ++--- .../igv/ui/action/LoadFromURLMenuAction.java | 184 +++++++++--------- .../igv/ui/action/OpenSessionMenuAction.java | 25 ++- .../org/broad/igv/ui/util/AutosaveMenu.java | 22 +-- .../org/broad/igv/ui/util/HistoryMenu.java | 13 +- .../broad/igv/ui/util/LoadFromURLDialog.java | 23 ++- .../org/broad/igv/ui/util/RecentUrlsMenu.java | 71 +++++++ .../org/broad/igv/util/ResourceLocator.java | 4 +- .../broad/igv/util/collections/LRUCache.java | 4 - .../broad/igv/sam/BisulfiteBaseInfoTest.java | 2 + .../org/broad/igv/ui/RecentFileSetTest.java | 56 ++++++ .../org/broad/igv/ui/RecentUrlsSetTest.java | 57 ++++++ .../java/org/broad/igv/ui/StackSetTest.java | 95 +++++++++ .../java/org/broad/igv/util/TestUtils.java | 1 + 23 files changed, 881 insertions(+), 298 deletions(-) create mode 100644 src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java create mode 100644 src/main/java/org/broad/igv/ui/MenuSelectedListener.java create mode 100644 src/main/java/org/broad/igv/ui/RecentFileSet.java create mode 100644 src/main/java/org/broad/igv/ui/RecentUrlsSet.java create mode 100644 src/main/java/org/broad/igv/ui/StackSet.java create mode 100644 src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java create mode 100644 src/test/java/org/broad/igv/ui/RecentFileSetTest.java create mode 100644 src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java create mode 100644 src/test/java/org/broad/igv/ui/StackSetTest.java diff --git a/src/main/java/org/broad/igv/prefs/Constants.java b/src/main/java/org/broad/igv/prefs/Constants.java index f87eb6c403..28f12ead80 100644 --- a/src/main/java/org/broad/igv/prefs/Constants.java +++ b/src/main/java/org/broad/igv/prefs/Constants.java @@ -42,6 +42,7 @@ private Constants() { // public static final String RECENT_SESSIONS = "IGV.Session.recent.sessions"; + public static final String RECENT_URLS = "IGV.Session.recent.urls"; public static final String LAST_EXPORTED_REGION_DIRECTORY = "LAST_EXPORTED_REGION_DIRECTORY"; public static final String LAST_TRACK_DIRECTORY = "LAST_TRACK_DIRECTORY"; public static final String LAST_SNAPSHOT_DIRECTORY = "LAST_SNAPSHOT_DIRECTORY"; diff --git a/src/main/java/org/broad/igv/prefs/IGVPreferences.java b/src/main/java/org/broad/igv/prefs/IGVPreferences.java index 58cafca3d6..497b9e2ca6 100644 --- a/src/main/java/org/broad/igv/prefs/IGVPreferences.java +++ b/src/main/java/org/broad/igv/prefs/IGVPreferences.java @@ -44,9 +44,7 @@ import org.broad.igv.renderer.SequenceRenderer; import org.broad.igv.sam.mods.BaseModificationColors; import org.broad.igv.track.TrackType; -import org.broad.igv.ui.IGV; -import org.broad.igv.ui.IGVMenuBar; -import org.broad.igv.ui.UIConstants; +import org.broad.igv.ui.*; import org.broad.igv.ui.color.ColorUtilities; import org.broad.igv.ui.color.PaletteColorTable; import org.broad.igv.ui.util.MessageUtils; @@ -280,7 +278,7 @@ public void put(String key, String value) { // Explicitly setting removes override overrideKeys.remove(key); - if (value == null || value.trim().length() == 0) { + if (value == null || value.isBlank()) { userPreferences.remove(key); } else { userPreferences.put(key, value); @@ -297,7 +295,7 @@ public void put(String key, boolean b) { public void putAll(Map updatedPrefs) { for (Map.Entry entry : updatedPrefs.entrySet()) { - if (entry.getValue() == null || entry.getValue().trim().length() == 0) { + if (entry.getValue() == null || entry.getValue().isBlank()) { remove(entry.getKey()); } else { put(entry.getKey(), entry.getValue()); @@ -610,14 +608,28 @@ public Rectangle getApplicationFrameBounds() { * @param recentSessions */ public void setRecentSessions(String recentSessions) { + remove(RECENT_SESSIONS); put(RECENT_SESSIONS, recentSessions); } - public String getRecentSessions() { - return get(RECENT_SESSIONS, null); + public RecentFileSet getRecentSessions() { + String sessionsString = get(RECENT_SESSIONS, null); + return RecentFileSet.fromString(sessionsString, UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST); } + public void setRecentUrls(String recentUrls) { + remove(RECENT_URLS); + put(RECENT_URLS, recentUrls); + } + + + public RecentUrlsSet getRecentUrls() { + String sessionsString = get(RECENT_URLS, null); + return RecentUrlsSet.fromString(sessionsString, UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST); + } + + public String getDataServerURL() { String masterResourceFile = get(DATA_SERVER_URL_KEY); return masterResourceFile; diff --git a/src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java b/src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java new file mode 100644 index 0000000000..b6b12d9996 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/DynamicMenuItemsAdjustmentListener.java @@ -0,0 +1,76 @@ +package org.broad.igv.ui; + +import javax.swing.*; +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +/** + * MenuListener which updates the availble menu items based on the contents of a changeable collection. + * + * Whenever the menu is selected the relevant JMenuItems are regenerated according to the current values in the backing + * collection. + * + * Items are inserted in a row beneath a given separator. + * + * @param element type of the collection + */ +public class DynamicMenuItemsAdjustmentListener implements MenuSelectedListener { + private final JMenu menu; + + private final JSeparator insertionPoint; + private final Collection values; + private final Function itemConstructor; + + //Store the currently visible components here so they can be removed when necessary + private final List activeComponents; + + /** + * + * @param menu the menu to modify + * @param insertionPoint a JSeparator which acts as an anchor to always insert elements below. This element is hidden + * when the collection is empty + * @param values a collection which is used to generate menu items + * @param itemConstructor a function to create a JMenuItem from an element in the collection + */ + public DynamicMenuItemsAdjustmentListener(JMenu menu, JSeparator insertionPoint, Collection values, Function itemConstructor) { + this.menu = menu; + this.insertionPoint = insertionPoint; + this.values = values; + this.itemConstructor = itemConstructor; + this.activeComponents = new ArrayList<>(); + } + + private List getCurrentItems() { + return values.stream().map(itemConstructor).toList(); + } + + @Override + public void menuSelected(MenuEvent e) { + List newComponents = getCurrentItems(); + + // We definitely don't want to be doing this while other things are also changing the menu + // this should at least protect against multiple of these listeners modifying the same menu at once. + synchronized (menu) { + activeComponents.forEach(menu::remove); + if (newComponents.isEmpty()) { + insertionPoint.setVisible(false); + } else { + insertionPoint.setVisible(true); + + final int componentIndex = Arrays.asList(menu.getMenuComponents()).indexOf(insertionPoint); + for (int i = 0; i < newComponents.size(); i++) { + menu.insert(newComponents.get(i), componentIndex + i + 1); + } + activeComponents.addAll(newComponents); + } + } + + menu.revalidate(); + menu.repaint(); + } +} diff --git a/src/main/java/org/broad/igv/ui/IGV.java b/src/main/java/org/broad/igv/ui/IGV.java index 9b71424e3e..ba830bdd58 100644 --- a/src/main/java/org/broad/igv/ui/IGV.java +++ b/src/main/java/org/broad/igv/ui/IGV.java @@ -134,9 +134,10 @@ public class IGV implements IGVEventObserver { private Timer sessionAutosaveTimer = new Timer(); // Misc state - private Map> overlayTracksMap = new HashMap(); - private Set overlaidTracks = new HashSet(); - private LinkedList recentSessionList = new LinkedList(); + private Map> overlayTracksMap = new HashMap<>(); + private Set overlaidTracks = new HashSet<>(); + private RecentFileSet recentSessionList; + private RecentUrlsSet recentUrlsList; // Vertical line that follows the mouse private boolean rulerEnabled; @@ -514,25 +515,15 @@ final public void doViewPreferences() { final public void saveStateForExit() { // Store recent sessions - if (!getRecentSessionList().isEmpty()) { - - int size = getRecentSessionList().size(); - if (size > UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST) { - size = UIConstants.NUMBER_OF_RECENT_SESSIONS_TO_LIST; - } - - String recentSessions = ""; - for (int i = 0; i < - size; i++) { - recentSessions += getRecentSessionList().get(i); - - if (i < (size - 1)) { - recentSessions += ";"; - } + RecentFileSet recentSessions = getRecentSessionList(); + if (!recentSessions.isEmpty()) { + PreferencesManager.getPreferences().setRecentSessions(recentSessions.asString()); + } - } - PreferencesManager.getPreferences().remove(RECENT_SESSIONS); - PreferencesManager.getPreferences().setRecentSessions(recentSessions); + // Store recent files + RecentUrlsSet recentUrls = getRecentUrls(); + if (!recentUrls.isEmpty()) { + PreferencesManager.getPreferences().setRecentUrls(recentUrls.asString()); } // Stop the timer that is triggering the timed autosave @@ -991,9 +982,8 @@ public boolean loadSession(String sessionPath, String locus) { } mainFrame.setTitle(UIConstants.APPLICATION_NAME + " - Session: " + sessionPath); - if (!recentSessionList.contains(sessionPath)) { - recentSessionList.addFirst(sessionPath); - } + + getRecentSessionList().add(sessionPath); this.menuBar.enableReloadSession(); //If there's a RegionNavigatorDialog, kill it. @@ -1006,7 +996,7 @@ public boolean loadSession(String sessionPath, String locus) { } catch (Exception e) { String message = "Error loading session session: " + e.getMessage(); MessageUtils.showMessage(message); - recentSessionList.remove(sessionPath); + getRecentSessionList().remove(sessionPath); log.error(e); return false; } finally { @@ -1068,9 +1058,8 @@ public void saveSession(File targetFile) throws IOException { String sessionPath = targetFile.getAbsolutePath(); session.setPath(sessionPath); mainFrame.setTitle(UIConstants.APPLICATION_NAME + " - Session: " + sessionPath); - if (!recentSessionList.contains(sessionPath)) { - recentSessionList.addFirst(sessionPath); - } + + getRecentSessionList().add(sessionPath); this.menuBar.enableReloadSession(); // No errors so save last location @@ -1130,10 +1119,36 @@ public MainPanel getMainPanel() { return contentPane.getMainPanel(); } - public LinkedList getRecentSessionList() { + public RecentFileSet getRecentSessionList() { + if(recentSessionList == null){ + recentSessionList = PreferencesManager.getPreferences().getRecentSessions(); + //remove sessions that no longer exist + recentSessionList.removeIf(file -> !(new File(file)).exists()); + } return recentSessionList; } + public RecentUrlsSet getRecentUrls() { + if(recentUrlsList == null){ + recentUrlsList = PreferencesManager.getPreferences().getRecentUrls(); + } + return recentUrlsList; + } + + /** + * Add new values to the recent URLS set. Calling this method rather than adding them directly + * allows showing the menu when the first URL is added to the collection. + * @param toAdd + */ + public void addToRecentUrls(Collection toAdd){ + RecentUrlsSet recentFiles = getRecentUrls(); + recentFiles.addAll(toAdd); + if(!recentFiles.isEmpty()){ + menuBar.showRecentFilesMenu(); + } + } + + public IGVContentPane getContentPane() { return contentPane; } @@ -1173,7 +1188,7 @@ public void loadResources(Collection locators) { for (final ResourceLocator locator : locators) { - // If its a local file, check explicitly for existence (rather than rely on exception) + // If it's a local file, check explicitly for existence (rather than rely on exception) if (locator.isLocal()) { File trackSetFile = new File(locator.getPath()); if (!trackSetFile.exists()) { @@ -1182,9 +1197,11 @@ public void loadResources(Collection locators) { } } + try { List tracks = load(locator); addTracks(tracks); + } catch (Exception e) { log.error("Error loading track", e); messages.append("Error loading " + locator + ": " + e.getMessage()); diff --git a/src/main/java/org/broad/igv/ui/IGVMenuBar.java b/src/main/java/org/broad/igv/ui/IGVMenuBar.java index c97b9e1424..6e36bc4496 100644 --- a/src/main/java/org/broad/igv/ui/IGVMenuBar.java +++ b/src/main/java/org/broad/igv/ui/IGVMenuBar.java @@ -36,7 +36,6 @@ import org.broad.igv.event.IGVEventBus; import org.broad.igv.event.IGVEventObserver; import org.broad.igv.feature.genome.Genome; -import org.broad.igv.feature.genome.GenomeDownloadUtils; import org.broad.igv.feature.genome.GenomeManager; import org.broad.igv.feature.genome.ChromSizesUtils; import org.broad.igv.track.AttributeManager; @@ -70,15 +69,13 @@ import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import javax.swing.plaf.basic.BasicBorders; -import javax.swing.plaf.basic.BasicMenuItemUI; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.*; import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; +import java.util.*; import java.util.List; import static org.broad.igv.prefs.Constants.*; @@ -114,10 +111,12 @@ public class IGVMenuBar extends JMenuBar implements IGVEventObserver { private JMenuItem loadGenomeFromServerMenuItem; private JMenuItem loadTracksFromServerMenuItem; private JMenuItem selectGenomeAnnotationsItem; - private JMenuItem encodeUCSCMenuItem; + private JMenuItem encodeUCSCMenuItem; private List encodeMenuItems = new ArrayList<>(); + private JMenuItem reloadSessionItem; + private JMenuItem recentFilesMenu; static IGVMenuBar createInstance(IGV igv) { @@ -299,6 +298,9 @@ JMenu createFileMenu() { loadTracksFromServerMenuItem = MenuAndToolbarUtils.createMenuItem(menuAction); menuItems.add(loadTracksFromServerMenuItem); + recentFilesMenu = new RecentUrlsMenu(); + menuItems.add(recentFilesMenu); + if (PreferencesManager.getPreferences().getAsBoolean(DB_ENABLED)) { menuAction = new LoadFromDatabaseAction("Load from Database...", 0, igv); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); @@ -407,39 +409,22 @@ public void actionPerformed(ActionEvent e) { menuAction.setToolTipText(EXIT_TOOLTIP); menuItems.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - - - // Empty the recent sessions list before we start to do - // anything with it - igv.getRecentSessionList().clear(); - - // Retrieve the stored session paths - String recentSessions = PreferencesManager.getPreferences().getRecentSessions(); - if (recentSessions != null) { - String[] sessions = recentSessions.split(";"); - for (String sessionPath : sessions) { - if (!sessionPath.equals("null") && - !igv.getRecentSessionList().contains(sessionPath) && - (new File(sessionPath)).exists()) { - igv.getRecentSessionList().add(sessionPath); - } - - } - } - - if (!igv.getRecentSessionList().isEmpty()) { - menuItems.add(new JSeparator()); - // Now add menu items - for (final String session : igv.getRecentSessionList()) { - OpenSessionMenuAction osMenuAction = new OpenSessionMenuAction(session, IGV.getInstance()); - menuItems.add(MenuAndToolbarUtils.createMenuItem(osMenuAction)); - } - - } - + JSeparator recentSessionsSep = new JSeparator(); + recentSessionsSep.setVisible(false); + menuItems.add(recentSessionsSep); + //menuItems.addAll(addRecentSessionMenuItems()); + menuItems.add(new JSeparator());; MenuAction fileMenuAction = new MenuAction("File", null, KeyEvent.VK_F); JMenu fileMenu = MenuAndToolbarUtils.createMenu(menuItems, fileMenuAction); + //Add dynamic list of recent sessions + fileMenu.addMenuListener(new DynamicMenuItemsAdjustmentListener<>( + fileMenu, + recentSessionsSep, + IGV.getInstance().getRecentSessionList(), + session -> MenuAndToolbarUtils.createMenuItem(new OpenSessionMenuAction(session, IGV.getInstance()))) + ); + return fileMenu; } @@ -539,22 +524,9 @@ public void actionPerformed(ActionEvent event) { menuAction.setToolTipText("Remove genomes which appear in the dropdown list"); menu.add(MenuAndToolbarUtils.createMenuItem(menuAction)); - menu.addMenuListener(new MenuListener() { - @Override - public void menuSelected(MenuEvent e) { - Genome genome = GenomeManager.getInstance().getCurrentGenome(); - selectGenomeAnnotationsItem.setEnabled(genome != null && genome.getHub() != null); - } - - @Override - public void menuDeselected(MenuEvent e) { - - } - - @Override - public void menuCanceled(MenuEvent e) { - - } + menu.addMenuListener((MenuSelectedListener) e -> { + Genome genome1 = GenomeManager.getInstance().getCurrentGenome(); + selectGenomeAnnotationsItem.setEnabled(genome1 != null && genome1.getHub() != null); }); return menu; @@ -1060,7 +1032,7 @@ private JMenu createAWSMenu() { loadS3.setEnabled(!usingCognito); // If using Cognito, disalbe initially menu.add(loadS3); - menu.addMenuListener(new MenuListener() { + menu.addMenuListener(new MenuSelectedListener() { @Override public void menuSelected(MenuEvent e) { if (AmazonUtils.GetCognitoConfig() != null) { @@ -1083,14 +1055,6 @@ public void menuSelected(MenuEvent e) { LongRunningTask.submit(runnable); } } - - @Override - public void menuDeselected(MenuEvent e) { - } - - @Override - public void menuCanceled(MenuEvent e) { - } }); @@ -1132,7 +1096,7 @@ private JMenu createGoogleMenu() { projectID.addActionListener(e -> GoogleUtils.enterGoogleProjectID()); googleMenu.add(projectID); - googleMenu.addMenuListener(new MenuListener() { + googleMenu.addMenuListener(new MenuSelectedListener() { @Override public void menuSelected(MenuEvent e) { boolean loggedIn = googleProvider.isLoggedIn(); @@ -1144,17 +1108,6 @@ public void menuSelected(MenuEvent e) { login.setEnabled(!loggedIn); logout.setEnabled(loggedIn); } - - @Override - public void menuDeselected(MenuEvent e) { - - } - - @Override - public void menuCanceled(MenuEvent e) { - - } - }); return googleMenu; @@ -1253,6 +1206,10 @@ public void enableReloadSession() { this.reloadSessionItem.setEnabled(true); } + public void showRecentFilesMenu(){ + this.recentFilesMenu.setVisible(true); + } + public void disableReloadSession() { this.reloadSessionItem.setEnabled(false); } @@ -1289,9 +1246,7 @@ private void exportTrackNames(final Collection selectedTracks) { return; } - PrintWriter pw = null; - try { - pw = new PrintWriter(new BufferedWriter(new FileWriter(file))); + try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)))) { List attributes = AttributeManager.getInstance().getVisibleAttributes(); @@ -1311,24 +1266,11 @@ private void exportTrackNames(final Collection selectedTracks) { } pw.println(); } - - } catch (IOException e) { MessageUtils.showErrorMessage("Error writing to file", e); log.error(e); - } finally { - if (pw != null) pw.close(); } - } -} - -class Foo extends BasicMenuItemUI { - - - public Foo() { - this.disabledForeground = Color.black; - } - } + diff --git a/src/main/java/org/broad/igv/ui/MenuSelectedListener.java b/src/main/java/org/broad/igv/ui/MenuSelectedListener.java new file mode 100644 index 0000000000..831f05dcdb --- /dev/null +++ b/src/main/java/org/broad/igv/ui/MenuSelectedListener.java @@ -0,0 +1,19 @@ +package org.broad.igv.ui; + +import javax.swing.event.MenuEvent; +import javax.swing.event.MenuListener; + +/** + * A menu listener which provides default noop implementations of menuDeselected and menuCancelled + */ +public interface MenuSelectedListener extends MenuListener { + + @Override + void menuSelected(MenuEvent e); + + @Override + default void menuDeselected(MenuEvent e) {} + + @Override + default void menuCanceled(MenuEvent e) {} +} diff --git a/src/main/java/org/broad/igv/ui/RecentFileSet.java b/src/main/java/org/broad/igv/ui/RecentFileSet.java new file mode 100644 index 0000000000..dcd27e171c --- /dev/null +++ b/src/main/java/org/broad/igv/ui/RecentFileSet.java @@ -0,0 +1,45 @@ +package org.broad.igv.ui; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * An implement of StackSet for local file paths which supports serializing/deserializing itself to a string. + * + * The serialized form matches the format which already existed to store recent session files. + * This is used by the by the + */ +public class RecentFileSet extends StackSet { + + private static final String DELIMITER = ";"; + + public RecentFileSet(int maxSize) { + super(maxSize); + } + + public RecentFileSet(Collection c, int maxSize) { + super(c, maxSize); + } + + public String asString() { + return String.join(DELIMITER, this); + } + + public static RecentFileSet fromString(String string, int maxSize) { + if(string == null || string.isBlank()){ + return new RecentFileSet(maxSize); + } + String[] files = string.split(DELIMITER); + List fileList = Arrays.stream(files) + .filter(s -> !s.isBlank()) + // "null" was previously accounted for in older code so it's handled here + // it doesn't seem like it should be possible to produce now though + .filter(s -> !s.equals("null")) + .map(String::strip) + .toList(); + return new RecentFileSet(fileList, maxSize); + } + + +} diff --git a/src/main/java/org/broad/igv/ui/RecentUrlsSet.java b/src/main/java/org/broad/igv/ui/RecentUrlsSet.java new file mode 100644 index 0000000000..e5019a0927 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/RecentUrlsSet.java @@ -0,0 +1,75 @@ +package org.broad.igv.ui; + +import org.broad.igv.ui.action.LoadFilesMenuAction; +import org.broad.igv.ui.action.LoadFromURLMenuAction; +import org.broad.igv.util.ResourceLocator; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * An implementation of StackSet which supports {@link ResourceLocator}s which include a path and an index + * This matches the form used by the {@link LoadFilesMenuAction} and {@link LoadFromURLMenuAction} + * + * + */ +public class RecentUrlsSet extends StackSet { + private static final String INDEX_DELIM = "index:"; + private static final Pattern INDEX_SPLITTER = Pattern.compile("\\s" + INDEX_DELIM); + + public RecentUrlsSet(int maxSize) { + super(maxSize); + } + + public RecentUrlsSet(Collection c, int maxSize) { + super(c, maxSize); + } + + public String asString(){ + return this.stream() + .map(RecentUrlsSet::locatorToString) + .collect(Collectors.joining("|")); + } + + private static String locatorToString(ResourceLocator locator) { + StringBuilder builder = new StringBuilder(); + builder.append(locator.getPath()); + if(locator.getIndexPath() != null) { + builder.append(" " + INDEX_DELIM); + builder.append(locator.getIndexPath()); + } + return builder.toString(); + } + + private static ResourceLocator stringToLocator(String locationString) { + String[] split = INDEX_SPLITTER.split(locationString); + + if (split.length != 1 && split.length != 2){ + return null; + } + final ResourceLocator result = new ResourceLocator(split[0].strip()); + if(split.length == 2) { + result.setIndexPath(split[1].strip()); + } + return result; + } + + public static RecentUrlsSet fromString(String urls, int maxLength){ + if(urls == null) { + return new RecentUrlsSet(maxLength); + } + + String[] elements = urls.split("\\|"); + List locators = Arrays.stream(elements) + .filter(Objects::nonNull) + .filter(elem -> !elem.isBlank()) + .map(RecentUrlsSet::stringToLocator) + .filter(Objects::nonNull) + .toList(); + return new RecentUrlsSet(locators, maxLength); + } +} diff --git a/src/main/java/org/broad/igv/ui/StackSet.java b/src/main/java/org/broad/igv/ui/StackSet.java new file mode 100644 index 0000000000..d33d6348be --- /dev/null +++ b/src/main/java/org/broad/igv/ui/StackSet.java @@ -0,0 +1,137 @@ +package org.broad.igv.ui; + +import java.util.*; + +/** + * This is a very specific collection which is designed to support "recent item" lists. + * + * It behaves like a stack, the most recently added item is always the first, second most recent is the second, and so forth + * It disallows duplicate items. Adding a duplicate item will move it to the top of the list. + * There is also size limit, adding additional items beyond the maximum size will remove the oldest items from the collection + * to make room. + * + * @implNote This is implemented in an extremely inefficient way, do not use this for large collections. + */ +public class StackSet extends AbstractCollection implements Set, SequencedCollection { + final private LinkedList values; + final int maxSize; + + /** + * Create an empty StackSet with maxSize + */ + public StackSet(int maxSize) { + this(new LinkedList<>(), maxSize); + } + + /** + * Constructs a StackSet containing the elements of the specified collection, in the order + * they are returned by the collection's iterator. (The first element returned by the collection's + * iterator becomes the first element, or top of the stack.) + * + * Duplicate values appear at their earliest position. + * + * This order consistent with the way {@link ArrayDeque} is implemented; + * + * @param initialValues + * @param maxSize + */ + public StackSet(Collection initialValues, int maxSize) { + this(new LinkedList<>(), maxSize); + Collection unique = new LinkedHashSet<>(initialValues); + List limited = unique.stream().limit(maxSize).toList(); + values.addAll(limited); + } + + + /** + * private constructor allowing direct setting of the values in order to support reverse views + */ + private StackSet(LinkedList values, int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize must be > 0"); + } + this.maxSize = maxSize; + this.values = values; + } + + /** + * Add an element to the top of the stack. If it is already present it will be + * moved to the top. + * @param t element to add + * @return always true + */ + @Override + public boolean add(T t) { + values.remove(t); + if (values.size() >= maxSize) { + values.removeLast(); + } + values.addFirst(t); + return true; + } + + /** + * Add the elements of this collection to the top of the stack in the order of the collections + * iterator. + * + * This is different from the implementation of {@link Deque} which adds to the end of the queue + * @param c collection containing elements to be added to this collection + * @return always true + */ + @Override + public boolean addAll(Collection c) { + ArrayList list = new ArrayList<>(c); + list.reversed().forEach(this::add); + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(Object o) { + return values.remove(o); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + values.clear(); + } + + /** + * @return an iterator over the elements of the Stack from most recently to oldest + */ + @Override + public Iterator iterator() { + return values.iterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return values.size(); + } + + /** + * @return the maximum allowed size for this collection + */ + public int getMaxSize(){ + return maxSize; + } + + /** + * {@inheritDoc} + */ + @Override + public StackSet reversed() { + return new StackSet<>(values.reversed(), maxSize); + } + +} + + diff --git a/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java index 4ef4ab0716..d3bdd82012 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFilesMenuAction.java @@ -30,9 +30,9 @@ package org.broad.igv.ui.action; import org.broad.igv.logging.*; -import org.broad.igv.Globals; import org.broad.igv.prefs.IGVPreferences; import org.broad.igv.prefs.PreferencesManager; +import org.broad.igv.session.SessionReader; import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.FileDialogUtils; import org.broad.igv.ui.util.MessageUtils; @@ -41,16 +41,16 @@ import java.awt.event.ActionEvent; import java.io.File; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; /** * @author jrobinso */ public class LoadFilesMenuAction extends MenuAction { - static Logger log = LogManager.getLogger(LoadFilesMenuAction.class); - IGV igv; + private static final Logger log = LogManager.getLogger(LoadFilesMenuAction.class); + private final IGV igv; public LoadFilesMenuAction(String label, int mnemonic, IGV igv) { super(label, null, mnemonic); @@ -59,9 +59,7 @@ public LoadFilesMenuAction(String label, int mnemonic, IGV igv) { @Override public void actionPerformed(ActionEvent e) { - loadFiles(chooseTrackFiles()); - } private File[] chooseTrackFiles() { @@ -85,25 +83,19 @@ private File[] chooseTrackFiles() { return trackFiles; } - private void loadFiles(File[] files) { + private void loadFiles(final File[] files) { if (files != null && files.length > 0) { - List validFileList = new ArrayList(); - StringBuffer buffer = new StringBuffer(); - buffer.append("File(s) not found: "); - boolean allFilesExist = true; - for (File file : files) { + final List validFiles = new ArrayList<>(); + final List missingFiles = new ArrayList<>(); + for (File file : files) { if (!file.exists()) { - allFilesExist = false; - buffer.append("\n\t"); - buffer.append(file.getAbsolutePath()); + missingFiles.add(file); } else { - String path = file.getAbsolutePath(); - if (path.endsWith(Globals.SESSION_FILE_EXTENSION)) { - // TODO -- a better test for session file than just the extension! + if (SessionReader.isSessionFile(path)) { final String msg = "File " + path + " appears to be an IGV Session file - " + "please use the Open Session menu item " + @@ -111,23 +103,25 @@ private void loadFiles(File[] files) { log.error(msg); MessageUtils.showMessage(msg); } else { - validFileList.add(file); + validFiles.add(file); } } } - files = validFileList.toArray(new File[validFileList.size()]); - if (!allFilesExist) { - final String msg = buffer.toString(); + if (!missingFiles.isEmpty()) { + String msg = missingFiles.stream() + .map(File::getAbsolutePath) + .collect(Collectors.joining("\n\t", "File(s) not found: \n\t", "")); log.error(msg); MessageUtils.showMessage(msg); } - if (files.length > 0) { - // Create DataResouceLocators for the selected files - final List locators = ResourceLocator.getLocators(Arrays.asList(files)); - igv.loadTracks(locators); + if (!validFiles.isEmpty()) { + // Create DataResourceLocators for the selected files + final List locators = ResourceLocator.getLocators(validFiles); + igv.addToRecentUrls(locators); + igv.loadTracks(locators); } } } diff --git a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java index 355bd8ff59..12823f77a4 100644 --- a/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/LoadFromURLMenuAction.java @@ -34,21 +34,18 @@ import org.broad.igv.feature.genome.load.HubGenomeLoader; import org.broad.igv.logging.*; import org.broad.igv.feature.genome.GenomeManager; -import org.broad.igv.util.GoogleUtils; -import org.broad.igv.prefs.Constants; -import org.broad.igv.prefs.PreferencesManager; import org.broad.igv.session.SessionReader; import org.broad.igv.ui.IGV; import org.broad.igv.ui.util.LoadFromURLDialog; import org.broad.igv.ui.util.MessageUtils; import org.broad.igv.util.*; -import org.broad.igv.ui.IGVMenuBar; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.io.IOException; import java.util.ArrayList; +import java.util.List; import static org.broad.igv.util.AmazonUtils.isObjectAccessible; @@ -57,12 +54,13 @@ */ public class LoadFromURLMenuAction extends MenuAction { - static Logger log = LogManager.getLogger(LoadFilesMenuAction.class); public static final String LOAD_FROM_URL = "Load from URL..."; public static final String LOAD_GENOME_FROM_URL = "Load Genome from URL..."; public static final String LOAD_FROM_HTSGET = "Load from htsget Server..."; public static final String LOAD_TRACKHUB = "Load Track Hub..."; - private IGV igv; + + private static final Logger log = LogManager.getLogger(LoadFromURLMenuAction.class); + private final IGV igv; public LoadFromURLMenuAction(String label, int mnemonic, IGV igv) { super(label, null, mnemonic); @@ -74,114 +72,106 @@ public void actionPerformed(ActionEvent e) { JPanel ta = new JPanel(); ta.setPreferredSize(new Dimension(600, 20)); - boolean isHtsGet = e.getActionCommand().equalsIgnoreCase(LOAD_FROM_HTSGET); - if (e.getActionCommand().equalsIgnoreCase(LOAD_FROM_URL) || isHtsGet) { + String command = e.getActionCommand(); + boolean isHtsGet = command.equalsIgnoreCase(LOAD_FROM_HTSGET); + if (command.equalsIgnoreCase(LOAD_FROM_URL) || isHtsGet) { LoadFromURLDialog dlg = new LoadFromURLDialog(IGV.getInstance().getMainFrame(), isHtsGet); dlg.setVisible(true); if (!dlg.isCanceled()) { - - - String inputURLs = dlg.getFileURL(); - if (inputURLs != null && inputURLs.trim().length() > 0) { - - String[] inputs = Globals.whitespacePattern.split(inputURLs.trim()); - checkURLs(inputs); - if (inputs.length == 1 && HubGenomeLoader.isHubURL(inputs[0])) { - LongRunningTask.submit(() -> { - try { - Genome newGenome = GenomeManager.getInstance().loadGenome(inputs[0]); - } catch (IOException ex) { - log.error("Error loading tack hub", ex); - MessageUtils.showMessage("Error loading track hub: " + ex.getMessage()); - - } - }); - } - else if (inputs.length == 1 && SessionReader.isSessionFile(inputs[0])) { - // Session URL - String url = inputs[0]; - if (url.startsWith("s3://")) { - checkAWSAccessbility(url); - } - try { - LongRunningTask.submit(() -> this.igv.loadSession(url, null)); - } catch (Exception ex) { - MessageUtils.showMessage("Error loading url: " + url + " (" + ex.toString() + ")"); - } - } else { - // Files, possibly indexed - String[] indexes = null; - String indexURLs = dlg.getIndexURL(); - if (indexURLs != null && indexURLs.trim().length() > 0) { - indexes = Globals.whitespacePattern.split(indexURLs.trim()); - if (indexes.length != inputs.length) { - throw new RuntimeException("The number of Index URLs must equal the number of File URLs"); - } - checkURLs(indexes); - } - - ArrayList locators = new ArrayList<>(); - for (int i = 0; i < inputs.length; i++) { - String url = inputs[i]; - ResourceLocator rl = new ResourceLocator(url.trim()); - if (indexes != null) { - String indexUrl = indexes[i]; - rl.setIndexPath(indexUrl); - } - if (isHtsGet) { - rl.setHtsget(true); - } - locators.add(rl); - } - igv.loadTracks(locators); - } - } + loadUrls(dlg.getFileURLs(), dlg.getIndexURLs(), isHtsGet); } - } else if ((e.getActionCommand().equalsIgnoreCase(LOAD_GENOME_FROM_URL))) { + } else if ((command.equalsIgnoreCase(LOAD_GENOME_FROM_URL))) { String url = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter URL to .genome or FASTA file", JOptionPane.QUESTION_MESSAGE); - if (url != null && url.trim().length() > 0) { - url = url.trim(); + loadGenomeFromUrl(url); + + } else if ((command.equalsIgnoreCase(LOAD_TRACKHUB))) { + loadTrackHub(ta); + } + } + + private void loadUrls(List inputs, List indexes, boolean isHtsGet) { + checkURLs(inputs); + if (inputs.size() == 1 && HubGenomeLoader.isHubURL(inputs.getFirst())) { + LongRunningTask.submit(() -> { try { - checkURLs(new String[]{url}); - GenomeManager.getInstance().loadGenome(url); - } catch (Exception e1) { - MessageUtils.showMessage("Error loading genome: " + e1.getMessage()); + GenomeManager.getInstance().loadGenome(inputs.getFirst()); + } catch (IOException ex) { + log.error("Error loading tack hub", ex); + MessageUtils.showMessage("Error loading track hub: " + ex.getMessage()); + } + }); + } else if (inputs.size() == 1 && SessionReader.isSessionFile(inputs.getFirst())) { + // Session URL + String url = inputs.getFirst(); + if (url.startsWith("s3://")) { + checkAWSAccessbility(url); + } + try { + LongRunningTask.submit(() -> this.igv.loadSession(url, null)); + } catch (Exception ex) { + MessageUtils.showMessage("Error loading url: " + url + " (" + ex + ")"); + } + } else { + if (!indexes.isEmpty() && indexes.size() != inputs.size()) { + throw new RuntimeException("The number of Index URLs must equal the number of File URLs"); + } + checkURLs(indexes); + List locators = getResourceLocators(inputs, indexes, isHtsGet); + igv.addToRecentUrls(locators); + igv.loadTracks(locators); + } + } + private static void loadTrackHub(JPanel ta) { + String urlOrAccension = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter GCA or GCF accession, or URL to hub.txt file", + JOptionPane.QUESTION_MESSAGE); + + if(urlOrAccension == null) return; + urlOrAccension = urlOrAccension.trim(); + final String url; + if(urlOrAccension.startsWith("GC")) { + url = HubGenomeLoader.convertToHubURL(urlOrAccension); + if(!FileUtils.resourceExists(url)) { + MessageUtils.showMessage("Unrecognized hub identifier: " + urlOrAccension); } - } else if ((e.getActionCommand().equalsIgnoreCase(LOAD_TRACKHUB))) { + } else { + url = urlOrAccension; + } - String urlOrAccension = JOptionPane.showInputDialog(IGV.getInstance().getMainFrame(), ta, "Enter GCA or GCF accension, or URL to hub.txt file", - JOptionPane.QUESTION_MESSAGE); + loadGenomeFromUrl(url); + } - if(urlOrAccension == null) return; - urlOrAccension = urlOrAccension.trim(); - String url; - if(urlOrAccension.startsWith("GC")) { - url = HubGenomeLoader.convertToHubURL(urlOrAccension); - if(url == null || !FileUtils.resourceExists(url)) { - MessageUtils.showMessage("Unrecognized hub identifier: " + urlOrAccension); - } - } else { - url = urlOrAccension; + private static void loadGenomeFromUrl(String url) { + if (url != null && !url.isBlank()) { + url = url.trim(); + try { + checkURLs(List.of(url)); + GenomeManager.getInstance().loadGenome(url); + } catch (Exception e) { + MessageUtils.showMessage("Error loading genome: " + e.getMessage()); } + } + } - if (url != null && url.trim().length() > 0) { - url = url.trim(); - try { - checkURLs(new String[]{url}); - GenomeManager.getInstance().loadGenome(url); - } catch (Exception e1) { - MessageUtils.showMessage("Error loading genome: " + e1.getMessage()); - } - + private static List getResourceLocators(Listinputs, List indexes, boolean isHtsGet) { + List locators = new ArrayList<>(); + for (int i = 0; i < inputs.size(); i++) { + final String url = inputs.get(i); + final ResourceLocator rl = new ResourceLocator(url.trim()); + if (!indexes.isEmpty()) { + final String indexUrl = indexes.get(i); + rl.setIndexPath(indexUrl); } + rl.setHtsget(isHtsGet); + locators.add(rl); } + return locators; } /** @@ -190,11 +180,11 @@ else if (inputs.length == 1 && SessionReader.isSessionFile(inputs[0])) { * @param input * @return */ - private boolean isHubURL(String input) { + private static boolean isHubURL(String input) { return input.endsWith("/hub.txt"); } - private void checkURLs(String[] urls) { + private static void checkURLs(List urls) { for (String url : urls) { if (url.startsWith("s3://")) { checkAWSAccessbility(url); @@ -204,7 +194,7 @@ private void checkURLs(String[] urls) { } } - private void checkAWSAccessbility(String url) { + private static void checkAWSAccessbility(String url) { try { // If AWS support is active, check if objects are in accessible tiers via Load URL menu... if (AmazonUtils.isAwsS3Path(url)) { diff --git a/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java b/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java index 3b1189627b..1bff4d9dd9 100644 --- a/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java +++ b/src/main/java/org/broad/igv/ui/action/OpenSessionMenuAction.java @@ -64,7 +64,7 @@ public OpenSessionMenuAction(String sessionFile, IGV igv) { super(sessionFile); this.sessionFile = sessionFile; this.igv = igv; - autoload = true; + this.autoload = true; } public OpenSessionMenuAction(String label, int mnemonic, IGV igv) { @@ -76,18 +76,25 @@ public OpenSessionMenuAction(String label, int mnemonic, IGV igv) { public void actionPerformed(ActionEvent e) { if (sessionFile == null || autoload == false) { - File lastSessionDirectory = PreferencesManager.getPreferences().getLastTrackDirectory(); - File tmpFile = FileDialogUtils.chooseFile("Open Session", lastSessionDirectory, JFileChooser.FILES_ONLY); - - if (tmpFile == null) { - return; - } - sessionFile = tmpFile.getAbsolutePath(); - PreferencesManager.getPreferences().setLastTrackDirectory(tmpFile.getParentFile()); + sessionFile = pickSessionFile(); } if (sessionFile != null) { LongRunningTask.submit(() -> this.igv.loadSession(sessionFile, null)); } } + + private static String pickSessionFile() { + File lastSessionDirectory = PreferencesManager.getPreferences().getLastTrackDirectory(); + File tmpFile = FileDialogUtils.chooseFile("Open Session", lastSessionDirectory, JFileChooser.FILES_ONLY); + + final String result; + if (tmpFile == null) { + result = null; + } else { + result = tmpFile.getAbsolutePath(); + PreferencesManager.getPreferences().setLastTrackDirectory(tmpFile.getParentFile()); + } + return result; + } } diff --git a/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java b/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java index b6069c9264..0c2579f77a 100644 --- a/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java +++ b/src/main/java/org/broad/igv/ui/util/AutosaveMenu.java @@ -2,6 +2,7 @@ import org.broad.igv.session.autosave.SessionAutosaveManager; import org.broad.igv.ui.IGV; +import org.broad.igv.ui.MenuSelectedListener; import org.broad.igv.ui.action.OpenSessionMenuAction; import javax.swing.*; @@ -24,22 +25,7 @@ public AutosaveMenu() { private AutosaveMenu(String name) { super(name); - this.addMenuListener(new MenuListener() { - @Override - public void menuSelected(MenuEvent e) { - fillAutosaveList(); - } - - @Override - public void menuDeselected(MenuEvent e) { - - } - - @Override - public void menuCanceled(MenuEvent e) { - - } - }); + this.addMenuListener((MenuSelectedListener) e -> fillAutosaveList()); } /** @@ -64,9 +50,9 @@ private void fillAutosaveList() { } } // Create a menu item for each of the timed autosave files and add it to the menu - for(int i = 0; i < timedAutosaves.length; i++) { + for (File timedAutosave : timedAutosaves) { add(MenuAndToolbarUtils.createMenuItem( - new OpenSessionMenuAction(timedAutosaves[i].getAbsolutePath(), IGV.getInstance()) + new OpenSessionMenuAction(timedAutosave.getAbsolutePath(), IGV.getInstance()) )); } } diff --git a/src/main/java/org/broad/igv/ui/util/HistoryMenu.java b/src/main/java/org/broad/igv/ui/util/HistoryMenu.java index 449264a9b2..140c126b9d 100644 --- a/src/main/java/org/broad/igv/ui/util/HistoryMenu.java +++ b/src/main/java/org/broad/igv/ui/util/HistoryMenu.java @@ -27,6 +27,7 @@ import org.broad.igv.ui.IGV; import org.broad.igv.session.History; +import org.broad.igv.ui.MenuSelectedListener; import javax.swing.*; import javax.swing.event.MenuEvent; @@ -77,14 +78,14 @@ public void actionPerformed(ActionEvent actionEvent) { }); - this.addMenuListener(new MenuListener() { + this.addMenuListener(new MenuSelectedListener() { public void menuSelected(MenuEvent menuEvent) { final History history = IGV.getInstance().getSession().getHistory(); List allLoci = IGV.getInstance().getSession().getAllHistory(); - + boolean hasBack = history.peekBack() != null; boolean hasForward = history.peekForward() != null; backItem.setEnabled(hasBack); @@ -121,14 +122,6 @@ public void actionPerformed(ActionEvent actionEvent) { } - - public void menuDeselected(MenuEvent menuEvent) { - //To change body of implemented methods use File | Settings | File Templates. - } - - public void menuCanceled(MenuEvent menuEvent) { - //To change body of implemented methods use File | Settings | File Templates. - } }); } diff --git a/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java b/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java index 7c75c448e8..de905c9923 100644 --- a/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java +++ b/src/main/java/org/broad/igv/ui/util/LoadFromURLDialog.java @@ -29,10 +29,13 @@ package org.broad.igv.ui.util; +import org.broad.igv.Globals; + import java.awt.*; import java.awt.event.*; -import java.net.MalformedURLException; -import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import javax.swing.*; import javax.swing.border.*; @@ -74,12 +77,20 @@ public boolean isCanceled() { return canceled; } - public String getFileURL() { - return fileURL; + public List getFileURLs() { + return splitOnWhiteSpace(fileURL); + } + + private static List splitOnWhiteSpace(String string) { + if (string != null && !string.isBlank()) { + String[] inputs = Globals.whitespacePattern.split(string.trim()); + return Arrays.asList(inputs); + } + return Collections.emptyList(); } - public String getIndexURL() { - return indexURL; + public List getIndexURLs() { + return splitOnWhiteSpace(indexURL); } private void initComponents(boolean isHtsget) { diff --git a/src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java b/src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java new file mode 100644 index 0000000000..055fcba687 --- /dev/null +++ b/src/main/java/org/broad/igv/ui/util/RecentUrlsMenu.java @@ -0,0 +1,71 @@ +package org.broad.igv.ui.util; + +import org.broad.igv.ui.IGV; +import org.broad.igv.ui.MenuSelectedListener; +import org.broad.igv.ui.RecentUrlsSet; +import org.broad.igv.ui.action.MenuAction; +import org.broad.igv.util.ResourceLocator; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.util.List; + +public class RecentUrlsMenu extends JMenu { + + public RecentUrlsMenu() { + this("Recent Files"); + } + + private RecentUrlsMenu(String name) { + super(name); + + this.addMenuListener((MenuSelectedListener) e -> { + RecentUrlsSet recentFileList = IGV.getInstance().getRecentUrls(); + if (recentFileList.isEmpty()) { + RecentUrlsMenu.this.setVisible(false); + } else { + RecentUrlsMenu.this.setVisible(true); + // Remove what's in the menu right now + RecentUrlsMenu.this.removeAll(); + // Create a menu item for each of the timed autosave files and add it to the menu + for (ResourceLocator resourceLocator : recentFileList) { + JMenuItem menuItem = createMenuItem(resourceLocator); + add(menuItem); + } + + addSeparator(); + JMenuItem clearButton = MenuAndToolbarUtils.createMenuItem(new MenuAction("Clear Recent Files") { + @Override + public void actionPerformed(ActionEvent event) { + IGV.getInstance().getRecentUrls().clear(); + RecentUrlsMenu.this.setVisible(false); + } + }); + add(clearButton); + } + }); + } + + private static JMenuItem createMenuItem(ResourceLocator resourceLocator) { + MenuAction menuItemAction = new MenuAction(resourceLocator.getPath()) { + @Override + public void actionPerformed(ActionEvent event) { + IGV igv = IGV.getInstance(); + List resource = List.of(resourceLocator); + igv.loadTracks(resource); + igv.addToRecentUrls(resource); + } + }; + + String toolTipText = resourceLocator.getIndexPath() == null + ? "Load track from " + resourceLocator.getPath() + : "Load track from
path: " + resourceLocator.getPath() + + "
index: " + resourceLocator.getIndexPath() + + ""; + + menuItemAction.setToolTipText(toolTipText); + return MenuAndToolbarUtils.createMenuItem(menuItemAction); + } +} + diff --git a/src/main/java/org/broad/igv/util/ResourceLocator.java b/src/main/java/org/broad/igv/util/ResourceLocator.java index ecf8a42a19..bb485cdde4 100644 --- a/src/main/java/org/broad/igv/util/ResourceLocator.java +++ b/src/main/java/org/broad/igv/util/ResourceLocator.java @@ -53,7 +53,7 @@ */ public class ResourceLocator { - private static Logger log = LogManager.getLogger(ResourceLocator.class); + private static final Logger log = LogManager.getLogger(ResourceLocator.class); /** * Display name @@ -736,7 +736,7 @@ public enum AttributeType { INDEX("index"), HTSGET("htsget"); - private String name; + private final String name; AttributeType(String name) { this.name = name; diff --git a/src/main/java/org/broad/igv/util/collections/LRUCache.java b/src/main/java/org/broad/igv/util/collections/LRUCache.java index d6fc90df99..097515d90d 100644 --- a/src/main/java/org/broad/igv/util/collections/LRUCache.java +++ b/src/main/java/org/broad/igv/util/collections/LRUCache.java @@ -46,10 +46,6 @@ public LRUCache(int max) { this.maxEntries = new AtomicInteger(max); } - public void setMaxEntries(int max) { - this.maxEntries.set(max); - } - private void createMap() { map = Collections.synchronizedMap( new LinkedHashMap(16, 0.75f, true) { diff --git a/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java b/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java index 5e8103df98..9b65801bc9 100644 --- a/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java +++ b/src/test/java/org/broad/igv/sam/BisulfiteBaseInfoTest.java @@ -29,9 +29,11 @@ import org.broad.igv.feature.LocusScore; import org.broad.igv.feature.Strand; import org.broad.igv.track.WindowFunction; +import org.junit.Assert; import org.junit.Test; import java.awt.*; +import java.util.LinkedHashSet; import java.util.List; /** diff --git a/src/test/java/org/broad/igv/ui/RecentFileSetTest.java b/src/test/java/org/broad/igv/ui/RecentFileSetTest.java new file mode 100644 index 0000000000..5591883ca7 --- /dev/null +++ b/src/test/java/org/broad/igv/ui/RecentFileSetTest.java @@ -0,0 +1,56 @@ +package org.broad.igv.ui; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class RecentFileSetTest { + + @Test + public void testAdds() { + RecentFileSet set1 = new RecentFileSet(3); + set1.add("a"); + set1.add("b"); + set1.add("c"); + set1.add("d"); + set1.add("e"); + StackSetTest.assertEquals(set1, List.of("e","d","c")); + } + + @Test + public void testAsListRoundTrip(){ + RecentFileSet set = new RecentFileSet(List.of("a", "b", "c", "d", "b"), 3); + StackSetTest.assertEquals(set, List.of("a", "b", "c")); + String string = set.asString(); + Assert.assertEquals("a;b;c", string); + RecentFileSet set2 = RecentFileSet.fromString(string, 5); + StackSetTest.assertEquals(set2, List.of("a", "b", "c")); + } + + @Test + public void testNullString(){ + RecentFileSet set = RecentFileSet.fromString(null, 5); + StackSetTest.assertEquals(set, List.of()); + } + + @Test + public void testEmptyString(){ + RecentFileSet set = RecentFileSet.fromString("", 5); + StackSetTest.assertEquals(set, List.of()); + } + + @Test + public void testWhiteSpaceString(){ + RecentFileSet set = RecentFileSet.fromString(" a; b ; c; ", 5); + StackSetTest.assertEquals(set, List.of("a", "b", "c")); + } + + @Test + public void testInternalWhiteSpaceString(){ + RecentFileSet set = RecentFileSet.fromString("this file has spaces;thisonedoesnt", 5); + StackSetTest.assertEquals(set, List.of("this file has spaces", "thisonedoesnt")); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java b/src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java new file mode 100644 index 0000000000..e21c16af2e --- /dev/null +++ b/src/test/java/org/broad/igv/ui/RecentUrlsSetTest.java @@ -0,0 +1,57 @@ +package org.broad.igv.ui; + +import org.broad.igv.util.ResourceLocator; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Iterator; +import java.util.List; + + +public class RecentUrlsSetTest { + + @Test + public void testRoundTrip() { + ResourceLocator withIndex1 = new ResourceLocator("path"); + withIndex1.setIndexPath("index"); + + ResourceLocator withIndex2 = new ResourceLocator("i have an index"); + withIndex2.setIndexPath("yes i do"); + + ResourceLocator noIndex1 = new ResourceLocator("path/no/index"); + ResourceLocator noIndex2 = new ResourceLocator("path 2"); + + RecentUrlsSet resourceLocators = new RecentUrlsSet(List.of(withIndex1, withIndex2, noIndex1, noIndex2, withIndex1), 5); + String serialized = resourceLocators.asString(); + Assert.assertEquals(serialized, "path index:index|i have an index index:yes i do|path/no/index|path 2"); + + RecentUrlsSet roundTrip = RecentUrlsSet.fromString(serialized, 5); + Assert.assertEquals(roundTrip.size(), resourceLocators.size()); + Iterator actual = roundTrip.iterator(); + Iterator expected = resourceLocators.iterator(); + while (actual.hasNext() && expected.hasNext()) { + assertEqualLocator(expected.next(), actual.next()); + } + } + + @Test + public void testEmpty(){ + RecentUrlsSet empty = new RecentUrlsSet(10); + Assert.assertEquals("", empty.asString()); + RecentUrlsSet fromEmptyString = RecentUrlsSet.fromString("", 5); + Assert.assertEquals(0, fromEmptyString.size()); + } + + @Test + public void testNull(){ + RecentUrlsSet empty = RecentUrlsSet.fromString(null, 5); + Assert.assertEquals(empty.size(),0); + } + + public static void assertEqualLocator(ResourceLocator expected, ResourceLocator actual){ + Assert.assertEquals(expected.getPath(), actual.getPath()); + Assert.assertEquals(expected.getIndexPath(), actual.getIndexPath()); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/broad/igv/ui/StackSetTest.java b/src/test/java/org/broad/igv/ui/StackSetTest.java new file mode 100644 index 0000000000..ddcb993a25 --- /dev/null +++ b/src/test/java/org/broad/igv/ui/StackSetTest.java @@ -0,0 +1,95 @@ +package org.broad.igv.ui; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class StackSetTest { + + + @Test + public void testAdds(){ + StackSet set = new StackSet<>(5); + set.add(1); + assertEquals(set, List.of(1)); + set.add(2); + assertEquals(set, List.of(2, 1)); + set.add(3); + assertEquals(set, List.of(3, 2, 1)); + set.add(4); + assertEquals(set, List.of(4, 3, 2, 1)); + set.add(5); + assertEquals(set, List.of(5, 4, 3, 2, 1)); + set.add(6); + assertEquals(set, List.of(6, 5, 4, 3, 2)); + set.add(7); + assertEquals(set, List.of(7, 6, 5, 4, 3)); + set.add(7); + assertEquals(set, List.of(7, 6, 5, 4, 3)); + set.add(1); + assertEquals(set, List.of(1, 7, 6, 5, 4)); + set.add(7); + assertEquals(set, List.of(7, 1, 6, 5, 4)); + set.remove(1); + assertEquals(set, List.of(7, 6, 5, 4)); + set.add(7); + assertEquals(set, List.of(7, 6, 5, 4)); + } + + @Test + public void testAddAll(){ + StackSet set = new StackSet<>(5); + set.add(1); + set.add(2); + set.add(3); + set.addAll(List.of(3,3,4,5,3)); + assertEquals(set, List.of(3,4,5,2,1)); + } + + @Test + public void testNewCollection(){ + StackSet set = new StackSet<>(List.of(1, 1, 2, 3, 4, 5, 6, 7, 3), 5); + assertEquals(set, List.of(1,2,3,4,5)); + set = new StackSet<>(List.of(1,1,1,1,1,1,1,1,1,1), 10); + assertEquals(set, List.of(1)); + set = new StackSet<>(Collections.emptySet(), 1); + assertEquals(set, List.of()); + set.add(1); + assertEquals(set, List.of(1)); + set.add(2); + assertEquals(set, List.of(2)); + } + + @Test + public void testDuplicateValuesPosition(){ + StackSet set = new StackSet<>(List.of(1, 2, 1, 3, 1, 4, 1, 5, 1), 5); + assertEquals(set, List.of(1,2,3,4,5)); + } + + @Test + public void testReverse(){ + StackSet set = new StackSet<>(List.of(1,2,3,4,5),5); + StackSet reversed = set.reversed(); + assertEquals(reversed, List.of(5,4,3,2,1)); + + reversed.add(1); + assertEquals(reversed, List.of(1,5,4,3,2)); + assertEquals(set, List.of(2,3,4,5,1)); + + set.remove((4)); + assertEquals(set, List.of(2,3,5,1)); + assertEquals(reversed, List.of(1,5,3,2)); + } + + + public static void assertEquals(Collection actual, List expected){ + Assert.assertEquals(expected.size(), actual.size()); + Assert.assertArrayEquals(expected.toArray(), actual.toArray()); + } + + +} \ No newline at end of file diff --git a/src/test/java/org/broad/igv/util/TestUtils.java b/src/test/java/org/broad/igv/util/TestUtils.java index 99f21fddaf..36629cea1f 100644 --- a/src/test/java/org/broad/igv/util/TestUtils.java +++ b/src/test/java/org/broad/igv/util/TestUtils.java @@ -42,6 +42,7 @@ import htsjdk.tribble.readers.AsciiLineReader; import org.junit.Assert; import org.junit.Ignore; +import org.junit.Test; import java.awt.*; import java.io.*;