├── .gitignore ├── docs └── images │ ├── OrganizerOverview.png │ ├── OrganizerOverview1_6.png │ └── SoulsSpeedruns_Banner.png ├── src └── com │ └── soulsspeedruns │ └── organizer │ ├── images │ ├── FrankerZ.png │ ├── DiscordLogo.png │ ├── ImportIcon.png │ ├── ImportIcon24.png │ ├── SettingsIcon.png │ ├── Splash_Screen.bmp │ ├── SoulsSpeedrunsLogo100.png │ ├── SoulsSpeedrunsLogo16.png │ ├── SoulsSpeedrunsLogo32.png │ ├── SoulsSpeedrunsLogo64.ico │ └── readonly │ │ ├── ReadOnlyIcon14.png │ │ ├── ReadOnlyIcon16.png │ │ ├── ReadOnlyIcon22.png │ │ ├── WritableIcon14.png │ │ ├── WritableIcon16.png │ │ ├── WritableIcon22.png │ │ ├── ReadOnlyIconSmall.png │ │ ├── ReadOnlyIconHover22.png │ │ ├── ReadOnlyIconMedium.png │ │ ├── WritableIconHover22.png │ │ ├── ReadOnlyIconDarkMode14.png │ │ ├── ReadOnlyIconDarkMode16.png │ │ ├── ReadOnlyIconDarkMode22.png │ │ ├── WritableIconDarkMode14.png │ │ ├── WritableIconDarkMode16.png │ │ ├── WritableIconDarkMode22.png │ │ ├── ReadOnlyIconDarkModeHover22.png │ │ └── WritableIconDarkModeHover22.png │ ├── theme │ ├── DefaultTheme.java │ ├── GlobalThemeInitTask.java │ ├── GlobalThemeAdjustmentTask.java │ ├── soulsspeedruns_icons.properties │ ├── SoulsSpeedrunsTheme.java │ ├── soulsspeedruns_ui.properties │ └── soulsspeedruns_defaults.properties │ ├── games │ ├── config │ │ ├── GameListListener.java │ │ ├── GameConfigPanel.java │ │ ├── ProfileListRenderer.java │ │ ├── GameListEntryDragListener.java │ │ ├── GamesConfigurationWindow.java │ │ ├── GameListTransferHandler.java │ │ ├── GameListEntry.java │ │ ├── GamesConfigPane.java │ │ ├── GameConfigProfilesPanel.java │ │ ├── GameConfigDirectoryPanel.java │ │ └── GameList.java │ └── Profile.java │ ├── listeners │ ├── SettingsListener.java │ ├── SearchListener.java │ ├── NavigationListener.java │ ├── SortingListener.java │ ├── GameListener.java │ ├── ProfileListener.java │ └── SaveListener.java │ ├── savelist │ ├── SaveListRenderer.java │ ├── SaveListDragListener.java │ ├── RootFolder.java │ ├── Save.java │ ├── SaveListTransferHandler.java │ ├── Folder.java │ └── SaveListEntry.java │ ├── main │ ├── config │ │ ├── SortingComboBoxRenderer.java │ │ ├── GamesComboBoxRenderer.java │ │ ├── ProfilesComboBoxRenderer.java │ │ ├── SortingComboBox.java │ │ ├── SortingCategory.java │ │ ├── GamesComboBox.java │ │ └── ProfilesComboBox.java │ ├── ListPanel.java │ ├── OrganizerWindow.java │ └── TopPanel.java │ ├── messages │ ├── FailedLoadMessage.java │ ├── SuccessfulImportMessage.java │ ├── SuccessfulRefreshMessage.java │ ├── SuccessfulDeleteMessage.java │ ├── SuccessfulLoadMessage.java │ ├── SuccessfulReplaceMessage.java │ ├── UndecoratedMessageDialog.java │ └── AbstractMessage.java │ ├── components │ ├── HyperLink.java │ ├── SearchBar.java │ └── ReadOnlyButton.java │ ├── update │ ├── NewReleasePanel.java │ └── NewReleaseWindow.java │ ├── settings │ ├── HotkeysSettingsPanel.java │ ├── ButtonsSettingsPanel.java │ ├── SettingsWindow.java │ ├── HotkeyTextField.java │ └── GeneralSettingsPanel.java │ ├── about │ ├── AboutPanel.java │ └── AboutWindow.java │ ├── hotkeys │ ├── GlobalHotkey.java │ └── GlobalKeyboardHook.java │ └── managers │ └── VersionManager.java ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /.classpath 3 | /.project 4 | /target/ 5 | /.settings 6 | /.vscode -------------------------------------------------------------------------------- /docs/images/OrganizerOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/docs/images/OrganizerOverview.png -------------------------------------------------------------------------------- /docs/images/OrganizerOverview1_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/docs/images/OrganizerOverview1_6.png -------------------------------------------------------------------------------- /docs/images/SoulsSpeedruns_Banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/docs/images/SoulsSpeedruns_Banner.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/FrankerZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/FrankerZ.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/DiscordLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/DiscordLogo.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/ImportIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/ImportIcon.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/ImportIcon24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/ImportIcon24.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/SettingsIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/SettingsIcon.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/Splash_Screen.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/Splash_Screen.bmp -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo100.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo16.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo32.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo64.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo64.ico -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIcon14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIcon14.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIcon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIcon16.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIcon22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIcon22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIcon14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIcon14.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIcon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIcon16.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIcon22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIcon22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconSmall.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconHover22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconHover22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconMedium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconMedium.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIconHover22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIconHover22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkMode14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkMode14.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkMode16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkMode16.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkMode22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkMode22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkMode14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkMode14.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkMode16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkMode16.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkMode22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkMode22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkModeHover22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/ReadOnlyIconDarkModeHover22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkModeHover22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kahmul/SoulsSpeedruns-Save-Organizer/HEAD/src/com/soulsspeedruns/organizer/images/readonly/WritableIconDarkModeHover22.png -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/theme/DefaultTheme.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.theme; 2 | 3 | import com.github.weisj.darklaf.theme.IntelliJTheme; 4 | 5 | public class DefaultTheme extends IntelliJTheme { 6 | 7 | @Override 8 | public String getName() { 9 | return "Default"; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameListListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | public interface GameListListener 5 | { 6 | 7 | public void entryCreated(GameListEntry entry); 8 | 9 | public void entryUpdated(GameListEntry entry); 10 | 11 | public void entryDeleted(GameListEntry entry); 12 | 13 | public void entrySelected(GameListEntry prevEntry, GameListEntry newEntry); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/listeners/SettingsListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.listeners; 2 | 3 | /** 4 | * Settings Listener. 5 | *

6 | * Provides methods to listen for changes to the settings. 7 | * 8 | * @author Kahmul (www.twitch.tv/kahmul78) 9 | * @date 18 Dec 2023 10 | */ 11 | public interface SettingsListener 12 | { 13 | 14 | /** 15 | * Called when the user navigates to next element 16 | */ 17 | public void settingChanged(String prefsKey); 18 | } 19 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/listeners/SearchListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.listeners; 2 | 3 | 4 | /** 5 | * Search Listener. 6 | *

7 | * Provides methods to listen for search requests. 8 | * 9 | * @author Kahmul (www.twitch.tv/kahmul78) 10 | * @date 19 May 2016 11 | */ 12 | public interface SearchListener 13 | { 14 | 15 | /** 16 | * Called when a search request has been sent. 17 | * 18 | * @param input the search input 19 | */ 20 | public void searchRequested(String input); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/listeners/NavigationListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.listeners; 2 | 3 | /** 4 | * Navigation Listener. 5 | *

6 | * Provides methods to listen for upwards/downwards navigation. 7 | * 8 | * @author johndisandonato (www.twitch.tv/johndisandonato) 9 | * @date 05 Sep 2018 10 | */ 11 | public interface NavigationListener 12 | { 13 | 14 | /** 15 | * Called when the user navigates to previous element 16 | */ 17 | public void navigatedToPrevious(); 18 | 19 | /** 20 | * Called when the user navigates to next element 21 | */ 22 | public void navigatedToNext(); 23 | } 24 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/listeners/SortingListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.listeners; 2 | 3 | 4 | import com.soulsspeedruns.organizer.main.config.SortingCategory; 5 | 6 | 7 | /** 8 | * Sorting Listener. 9 | *

10 | * Provides methods to listen for sorting category changes. 11 | * 12 | * @author Kahmul (www.twitch.tv/kahmul78) 13 | * @date 20 May 2016 14 | */ 15 | public interface SortingListener 16 | { 17 | 18 | /** 19 | * Called when the user chooses a different sorting option. 20 | * 21 | * @param category the category that was chosen 22 | */ 23 | public void sortingChanged(SortingCategory category); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/theme/GlobalThemeInitTask.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.theme; 2 | 3 | import javax.swing.UIDefaults; 4 | import javax.swing.plaf.InsetsUIResource; 5 | 6 | import com.github.weisj.darklaf.task.DefaultsInitTask; 7 | import com.github.weisj.darklaf.theme.Theme; 8 | 9 | public class GlobalThemeInitTask implements DefaultsInitTask 10 | { 11 | 12 | @Override 13 | public void run(Theme currentTheme, UIDefaults defaults) 14 | { 15 | defaults.put("Button.rollover", true); 16 | defaults.put("ComboBox.squareButton", true); 17 | defaults.put("ComboBox.valueInsets", new InsetsUIResource(4, 4, 4, 4)); 18 | defaults.put("MenuItem.insets", new InsetsUIResource(3, 2, 3, 4)); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/savelist/SaveListRenderer.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.savelist; 2 | 3 | import java.awt.Component; 4 | 5 | import javax.swing.DefaultListCellRenderer; 6 | import javax.swing.JLabel; 7 | import javax.swing.JList; 8 | import javax.swing.ListCellRenderer; 9 | 10 | public class SaveListRenderer implements ListCellRenderer 11 | { 12 | 13 | private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); 14 | 15 | @Override 16 | public Component getListCellRendererComponent(JList list, SaveListEntry entry, int index, 17 | boolean isSelected, boolean cellHasFocus) 18 | { 19 | JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent(list, entry, index, isSelected, cellHasFocus); 20 | entry.render(list, index, label); 21 | 22 | return label; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/config/SortingComboBoxRenderer.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main.config; 2 | 3 | import java.awt.Component; 4 | 5 | import javax.swing.DefaultListCellRenderer; 6 | import javax.swing.JLabel; 7 | import javax.swing.JList; 8 | import javax.swing.ListCellRenderer; 9 | 10 | public class SortingComboBoxRenderer implements ListCellRenderer 11 | { 12 | 13 | private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); 14 | 15 | @Override 16 | public Component getListCellRendererComponent(JList list, SortingCategory category, 17 | int index, boolean isSelected, boolean cellHasFocus) 18 | { 19 | JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent(list, category, index, isSelected, cellHasFocus); 20 | if (category != null) 21 | label.setText(category.getCaption()); 22 | return label; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/config/GamesComboBoxRenderer.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main.config; 2 | 3 | import java.awt.Component; 4 | 5 | import javax.swing.DefaultListCellRenderer; 6 | import javax.swing.JLabel; 7 | import javax.swing.JList; 8 | import javax.swing.ListCellRenderer; 9 | 10 | import com.soulsspeedruns.organizer.games.Game; 11 | 12 | public class GamesComboBoxRenderer implements ListCellRenderer 13 | { 14 | 15 | private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); 16 | 17 | @Override 18 | public Component getListCellRendererComponent(JList list, Game game, int index, boolean isSelected, 19 | boolean cellHasFocus) 20 | { 21 | JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent(list, game, index, isSelected, cellHasFocus); 22 | if (game != null) 23 | label.setText(game.getCaption()); 24 | return label; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/config/ProfilesComboBoxRenderer.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main.config; 2 | 3 | import java.awt.Component; 4 | 5 | import javax.swing.DefaultListCellRenderer; 6 | import javax.swing.JLabel; 7 | import javax.swing.JList; 8 | import javax.swing.ListCellRenderer; 9 | 10 | import com.soulsspeedruns.organizer.games.Profile; 11 | 12 | public class ProfilesComboBoxRenderer implements ListCellRenderer 13 | { 14 | 15 | private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); 16 | 17 | @Override 18 | public Component getListCellRendererComponent(JList list, Profile profile, int index, 19 | boolean isSelected, boolean cellHasFocus) 20 | { 21 | JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent(list, profile, index, isSelected, cellHasFocus); 22 | if (profile != null) 23 | label.setText(profile.getName()); 24 | return label; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameConfigPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import javax.swing.BoxLayout; 5 | import javax.swing.JPanel; 6 | import javax.swing.JSeparator; 7 | 8 | import com.soulsspeedruns.organizer.games.Game; 9 | 10 | 11 | /** 12 | * Game Configuration Panel. 13 | *

14 | * Panel containing components to configure game settings and their respective 15 | * profiles. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 28 Sep 2015 19 | */ 20 | public class GameConfigPanel extends JPanel 21 | { 22 | 23 | 24 | /** 25 | * Creates a game configuration panel. 26 | * 27 | * @param game the game to create the panel for 28 | */ 29 | protected GameConfigPanel(Game game) 30 | { 31 | setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); 32 | 33 | add(new GameConfigDirectoryPanel(game)); 34 | add(new JSeparator()); 35 | add(new GameConfigProfilesPanel(game)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/listeners/GameListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.listeners; 2 | 3 | import com.soulsspeedruns.organizer.games.Game; 4 | 5 | /** 6 | * Game Listener. 7 | *

8 | * Provides methods to listen for game changes. 9 | * 10 | * @author Kahmul (www.twitch.tv/kahmul78) 11 | * @date 12 Jan 2024 12 | */ 13 | public interface GameListener 14 | { 15 | 16 | /** 17 | * Called when a new game has been created. 18 | * 19 | * @param game the game that was created 20 | */ 21 | public void gameCreated(Game game); 22 | 23 | /** 24 | * Called when a game has been deleted. 25 | * 26 | * @param game the game that was deleted. 27 | */ 28 | public void gameDeleted(Game game); 29 | 30 | 31 | /** 32 | * Called when a game has been edited or changed. 33 | * 34 | * @param game the game that was edited or changed. 35 | */ 36 | public void gameEdited(Game game); 37 | 38 | 39 | public void gameMoved(Game game, int newIndex); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/ProfileListRenderer.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | import java.awt.Component; 4 | 5 | import javax.swing.BorderFactory; 6 | import javax.swing.DefaultListCellRenderer; 7 | import javax.swing.JLabel; 8 | import javax.swing.JList; 9 | import javax.swing.ListCellRenderer; 10 | 11 | import com.soulsspeedruns.organizer.games.Profile; 12 | 13 | public class ProfileListRenderer implements ListCellRenderer 14 | { 15 | 16 | private final DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer(); 17 | 18 | @Override 19 | public Component getListCellRendererComponent(JList list, Profile profile, int index, 20 | boolean isSelected, boolean cellHasFocus) 21 | { 22 | JLabel label = (JLabel) defaultRenderer.getListCellRendererComponent(list, profile, index, isSelected, cellHasFocus); 23 | if (profile != null) 24 | { 25 | label.setText(profile.getName()); 26 | label.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 0)); 27 | } 28 | return label; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/theme/GlobalThemeAdjustmentTask.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.theme; 2 | 3 | 4 | import java.awt.Color; 5 | import java.util.Properties; 6 | 7 | import com.github.weisj.darklaf.task.DefaultsAdjustmentTask; 8 | import com.github.weisj.darklaf.theme.Theme; 9 | 10 | 11 | public class GlobalThemeAdjustmentTask implements DefaultsAdjustmentTask 12 | { 13 | 14 | @Override 15 | public void run(Theme currentTheme, Properties properties) 16 | { 17 | // fixes the title pane text not being the proper font color with decorations enabled 18 | properties.put("textForegroundSecondary", properties.get("textForeground")); 19 | properties.put("borderThickness", 0); 20 | properties.put("arc", 0); 21 | properties.put("disabledIconColor", Theme.isDark(currentTheme) ? Color.decode("#495162") : Color.decode("#bfc0c1")); 22 | properties.put("gameConfigButtonColors", currentTheme.getClass().equals(SoulsSpeedrunsTheme.class) ? Color.decode("#fdba74") 23 | : Theme.isHighContrast(currentTheme) ? properties.get("hyperlink") : Color.decode("#4D89C9")); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/config/SortingComboBox.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main.config; 2 | 3 | 4 | import java.awt.event.ItemEvent; 5 | 6 | import javax.swing.JComboBox; 7 | 8 | import com.soulsspeedruns.organizer.managers.SavesManager; 9 | 10 | 11 | /** 12 | * SortingComboBox 13 | *

14 | * ComboBox displaying the sorting options. 15 | * 16 | * @author Kahmul (www.twitch.tv/kahmul78) 17 | * @date 18 May 2016 18 | */ 19 | public class SortingComboBox extends JComboBox 20 | { 21 | 22 | /** 23 | * Creates a new sorting combobox. 24 | */ 25 | public SortingComboBox() 26 | { 27 | for (SortingCategory category : SortingCategory.values()) 28 | { 29 | addItem(category); 30 | if (category == SavesManager.getSelectedSortingCategory()) 31 | setSelectedItem(category); 32 | } 33 | 34 | setRenderer(new SortingComboBoxRenderer()); 35 | addItemListener(event -> { 36 | if (event.getStateChange() == ItemEvent.SELECTED) 37 | { 38 | SavesManager.setSelectedSortingCategory((SortingCategory) event.getItem()); 39 | } 40 | }); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/FailedLoadMessage.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.Color; 5 | 6 | import javax.swing.Icon; 7 | 8 | import jiconfont.icons.Iconic; 9 | import jiconfont.swing.IconFontSwing; 10 | 11 | 12 | /** 13 | * Short description. 14 | *

15 | * Long description. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 14 Dec 2023 19 | */ 20 | public class FailedLoadMessage extends AbstractMessage 21 | { 22 | 23 | private static final String MESSAGE = "LOAD FAILED"; 24 | private static final Color COLOR = Color.decode("0xee6a5c"); 25 | private static final int ICON_SIZE = 22; 26 | private static final Icon ICON = IconFontSwing.buildIcon(Iconic.CHECK, ICON_SIZE, COLOR); 27 | 28 | 29 | protected FailedLoadMessage() 30 | { 31 | super(); 32 | } 33 | 34 | 35 | @Override 36 | protected String getMessage() 37 | { 38 | return MESSAGE; 39 | } 40 | 41 | 42 | @Override 43 | protected Icon getIcon() 44 | { 45 | return ICON; 46 | } 47 | 48 | 49 | @Override 50 | protected int getIconSize() { 51 | return ICON_SIZE; 52 | } 53 | 54 | 55 | @Override 56 | protected Color getColor() 57 | { 58 | return COLOR; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/SuccessfulImportMessage.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.Color; 5 | 6 | import javax.swing.Icon; 7 | 8 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 9 | 10 | 11 | /** 12 | * SuccessfulImportMessage. 13 | *

14 | * AbstractMessage class implementation for an Import message. 15 | * 16 | * @author Kahmul (www.twitch.tv/kahmul78) 17 | * @date 15 Jul 2017 18 | */ 19 | public class SuccessfulImportMessage extends AbstractMessage 20 | { 21 | 22 | private static final String MESSAGE = "IMPORT SUCCESSFUL"; 23 | private static final Icon ICON = IconsAndFontsManager.getImportIcon(IconsAndFontsManager.ICON_SIZE_LARGE); 24 | private static final Color COLOR = Color.decode("0x1d6fbe"); 25 | 26 | 27 | protected SuccessfulImportMessage() 28 | { 29 | super(); 30 | } 31 | 32 | 33 | @Override 34 | protected String getMessage() 35 | { 36 | return MESSAGE; 37 | } 38 | 39 | 40 | @Override 41 | protected Icon getIcon() 42 | { 43 | return ICON; 44 | } 45 | 46 | 47 | @Override 48 | protected int getIconSize() { 49 | return 22; 50 | } 51 | 52 | 53 | @Override 54 | protected Color getColor() 55 | { 56 | return COLOR; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/SuccessfulRefreshMessage.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.Color; 5 | 6 | import javax.swing.Icon; 7 | 8 | import jiconfont.icons.Elusive; 9 | import jiconfont.swing.IconFontSwing; 10 | 11 | 12 | /** 13 | * Short description. 14 | *

15 | * Long description. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 13 Apr 2018 19 | */ 20 | public class SuccessfulRefreshMessage extends AbstractMessage 21 | { 22 | 23 | private static final String MESSAGE = "REFRESH SUCCESSFUL"; 24 | private static final Color COLOR = Color.decode("0x1d6fbe"); 25 | private static final int ICON_SIZE = 20; 26 | private static final Icon ICON = IconFontSwing.buildIcon(Elusive.REPEAT, ICON_SIZE, COLOR); 27 | 28 | 29 | protected SuccessfulRefreshMessage() 30 | { 31 | super(); 32 | } 33 | 34 | 35 | @Override 36 | protected String getMessage() 37 | { 38 | return MESSAGE; 39 | } 40 | 41 | 42 | @Override 43 | protected Icon getIcon() 44 | { 45 | return ICON; 46 | } 47 | 48 | 49 | @Override 50 | protected int getIconSize() { 51 | return ICON_SIZE; 52 | } 53 | 54 | 55 | @Override 56 | protected Color getColor() 57 | { 58 | return COLOR; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/SuccessfulDeleteMessage.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.Color; 5 | 6 | import javax.swing.Icon; 7 | 8 | import jiconfont.icons.FontAwesome; 9 | import jiconfont.swing.IconFontSwing; 10 | 11 | 12 | /** 13 | * Short description. 14 | *

15 | * Long description. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 2 Mar 2018 19 | */ 20 | public class SuccessfulDeleteMessage extends AbstractMessage 21 | { 22 | 23 | private static final String MESSAGE = "DELETE SUCCESSFUL"; 24 | private static final Color COLOR = Color.decode("0xea3622"); 25 | private static final int ICON_SIZE = 22; 26 | private static final Icon ICON = IconFontSwing.buildIcon(FontAwesome.CHECK, ICON_SIZE, COLOR); 27 | 28 | 29 | protected SuccessfulDeleteMessage() 30 | { 31 | super(); 32 | } 33 | 34 | 35 | @Override 36 | protected String getMessage() 37 | { 38 | return MESSAGE; 39 | } 40 | 41 | 42 | @Override 43 | protected Icon getIcon() 44 | { 45 | return ICON; 46 | } 47 | 48 | 49 | @Override 50 | protected int getIconSize() { 51 | return ICON_SIZE; 52 | } 53 | 54 | 55 | @Override 56 | protected Color getColor() 57 | { 58 | return COLOR; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/SuccessfulLoadMessage.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.Color; 5 | 6 | import javax.swing.Icon; 7 | 8 | import jiconfont.icons.Elusive; 9 | import jiconfont.swing.IconFontSwing; 10 | 11 | 12 | /** 13 | * SuccessfulLoadMessage. 14 | *

15 | * AbstractMessage class implementation for a Load message. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 15 Jul 2017 19 | */ 20 | public class SuccessfulLoadMessage extends AbstractMessage 21 | { 22 | 23 | private static final String MESSAGE = "LOAD SUCCESSFUL"; 24 | private static final Color COLOR = Color.decode("0x2c9558"); 25 | private static final int ICON_SIZE = 20; 26 | private static final Icon ICON = IconFontSwing.buildIcon(Elusive.REPEAT, ICON_SIZE, COLOR); 27 | 28 | 29 | protected SuccessfulLoadMessage() 30 | { 31 | super(); 32 | } 33 | 34 | 35 | @Override 36 | protected String getMessage() 37 | { 38 | return MESSAGE; 39 | } 40 | 41 | 42 | @Override 43 | protected Icon getIcon() 44 | { 45 | return ICON; 46 | } 47 | 48 | 49 | @Override 50 | protected int getIconSize() { 51 | return ICON_SIZE; 52 | } 53 | 54 | 55 | @Override 56 | protected Color getColor() 57 | { 58 | return COLOR; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/SuccessfulReplaceMessage.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.Color; 5 | 6 | import javax.swing.Icon; 7 | 8 | import jiconfont.icons.Elusive; 9 | import jiconfont.swing.IconFontSwing; 10 | 11 | 12 | /** 13 | * SuccessfulReplaceMessage. 14 | *

15 | * AbstractMessage class implementation for a Replace message. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 15 Jul 2017 19 | */ 20 | public class SuccessfulReplaceMessage extends AbstractMessage 21 | { 22 | 23 | private static final String MESSAGE = "REPLACE SUCCESSFUL"; 24 | private static final Color COLOR = Color.decode("0xeb751c"); 25 | private static final int ICON_SIZE = 20; 26 | private static final Icon ICON = IconFontSwing.buildIcon(Elusive.REFRESH, ICON_SIZE, COLOR); 27 | 28 | 29 | protected SuccessfulReplaceMessage() 30 | { 31 | super(); 32 | } 33 | 34 | 35 | @Override 36 | protected String getMessage() 37 | { 38 | return MESSAGE; 39 | } 40 | 41 | 42 | @Override 43 | protected Icon getIcon() 44 | { 45 | return ICON; 46 | } 47 | 48 | 49 | @Override 50 | protected int getIconSize() { 51 | return ICON_SIZE; 52 | } 53 | 54 | 55 | @Override 56 | protected Color getColor() 57 | { 58 | return COLOR; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/listeners/ProfileListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.listeners; 2 | 3 | 4 | import com.soulsspeedruns.organizer.games.Game; 5 | import com.soulsspeedruns.organizer.games.Profile; 6 | 7 | 8 | /** 9 | * Profile Listener. 10 | *

11 | * Provides methods to listen for profile changes. 12 | * 13 | * @author Kahmul (www.twitch.tv/kahmul78) 14 | * @date 29 Sep 2015 15 | */ 16 | public interface ProfileListener 17 | { 18 | 19 | /** 20 | * Called when a profile has been deleted. 21 | * 22 | * @param profile the deleted profile 23 | */ 24 | public void profileDeleted(Profile profile); 25 | 26 | 27 | /** 28 | * Called when a profile has been created. 29 | * 30 | * @param profile the created profile 31 | */ 32 | public void profileCreated(Profile profile); 33 | 34 | 35 | /** 36 | * Called when the user changes the profile directory in the config window. 37 | */ 38 | public void profileDirectoryChanged(Game game); 39 | 40 | 41 | /** 42 | * Called when the user switches to another profile. 43 | * 44 | * @param profile the profile that was switched to 45 | */ 46 | public void changedToProfile(Profile profile); 47 | 48 | 49 | /** 50 | * Called when the user switches to another game. 51 | * 52 | * @param game the game that was switched to 53 | */ 54 | public void changedToGame(Game game); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/ListPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main; 2 | 3 | 4 | import javax.swing.GroupLayout; 5 | import javax.swing.GroupLayout.Alignment; 6 | import javax.swing.JPanel; 7 | import javax.swing.ScrollPaneConstants; 8 | 9 | import com.github.weisj.darklaf.components.OverlayScrollPane; 10 | import com.soulsspeedruns.organizer.savelist.SaveList; 11 | 12 | 13 | /** 14 | * List Panel. 15 | *

16 | * Contains the list with savestates. 17 | * 18 | * @author Kahmul (www.twitch.tv/kahmul78) 19 | * @date 27 Sep 2015 20 | */ 21 | public class ListPanel extends JPanel 22 | { 23 | 24 | /** 25 | * Creates a new ListPanel for the main window. 26 | */ 27 | protected ListPanel() 28 | { 29 | GroupLayout layout = new GroupLayout(this); 30 | 31 | SaveList saveList = new SaveList(); 32 | 33 | OverlayScrollPane savePane = new OverlayScrollPane(saveList, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, 34 | ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); 35 | 36 | // Horizontal 37 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 38 | 39 | hGroup.addGap(12); 40 | hGroup.addGroup(layout.createParallelGroup().addComponent(savePane)); 41 | hGroup.addGap(12); 42 | 43 | layout.setHorizontalGroup(hGroup); 44 | 45 | // Vertical 46 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 47 | 48 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(savePane)); 49 | 50 | layout.setVerticalGroup(vGroup); 51 | 52 | setLayout(layout); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/theme/soulsspeedruns_icons.properties: -------------------------------------------------------------------------------- 1 | %menuIconOpacity = 80 2 | %navigationIconOpacity = 50 3 | %fileIconOpacity = 80 4 | 5 | %menuIconEnabled = #abb2bf 6 | %menuIconHovered = #F9FCFF 7 | %menuIconSelected = #F9FCFF 8 | %menuIconSelectedSecondary = #F9FCFF 9 | %menuIconDisabled = #2c313a 10 | %menuIconHighlight = #337C97 11 | 12 | %fileIconBackground = #828D96 13 | %fileIconForeground = #404043 14 | %fileIconHighlight = #499C54 15 | 16 | %textIconEnabled = %textForeground 17 | %textIconDisabled = %textForegroundInactive 18 | %textIconSelected = %textSelectionForeground 19 | 20 | %windowButton = #A9A9A9 21 | %windowButtonDisabled = #8C8C8C 22 | %windowCloseHovered = #FFFFFF 23 | 24 | %errorIconColor = #802d43 25 | %informationIconColor = #4269b9 26 | %warningIconColor = #8c812b 27 | %questionIconColor = #4269b9 28 | 29 | %palette.yellow = #E5C07B 30 | %palette.orange = #FF8C00 31 | %palette.red = #EF596F 32 | %palette.pink = #C678DD 33 | %palette.purple = #A03BA8 34 | %palette.indigo = #5E5CE6 35 | %palette.blue = #0A84FF 36 | %palette.teal = #61AFEF 37 | %palette.cyan = #2BBAC5 38 | %palette.green = #89CA78 39 | %palette.lime = #69B320 40 | %palette.forest = #13591E 41 | %palette.brown = #AC8E68 42 | %palette.gray = #ABB2BF 43 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameListEntryDragListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import java.awt.dnd.DnDConstants; 5 | import java.awt.dnd.DragGestureEvent; 6 | import java.awt.dnd.DragGestureListener; 7 | import java.awt.dnd.DragSource; 8 | import java.awt.dnd.DragSourceDragEvent; 9 | import java.awt.dnd.DragSourceDropEvent; 10 | import java.awt.dnd.DragSourceEvent; 11 | import java.awt.dnd.DragSourceListener; 12 | 13 | 14 | public class GameListEntryDragListener implements DragSourceListener, DragGestureListener 15 | { 16 | 17 | private GameListEntry entry; 18 | private DragSource dragSource; 19 | private GameList list; 20 | 21 | 22 | public GameListEntryDragListener(GameListEntry entry, GameList list) 23 | { 24 | this.entry = entry; 25 | this.list = list; 26 | 27 | dragSource = new DragSource(); 28 | dragSource.createDefaultDragGestureRecognizer(entry, DnDConstants.ACTION_MOVE, this); 29 | } 30 | 31 | 32 | @Override 33 | public void dragGestureRecognized(DragGestureEvent dge) 34 | { 35 | dragSource.startDrag(dge, null, entry, this); 36 | } 37 | 38 | 39 | @Override 40 | public void dragEnter(DragSourceDragEvent dsde) 41 | { 42 | } 43 | 44 | 45 | @Override 46 | public void dragOver(DragSourceDragEvent dsde) 47 | { 48 | } 49 | 50 | 51 | @Override 52 | public void dropActionChanged(DragSourceDragEvent dsde) 53 | { 54 | } 55 | 56 | 57 | @Override 58 | public void dragExit(DragSourceEvent dse) 59 | { 60 | list.clearDropTarget(); 61 | } 62 | 63 | 64 | @Override 65 | public void dragDropEnd(DragSourceDropEvent dsde) 66 | { 67 | list.clearDropTarget(); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/listeners/SaveListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.listeners; 2 | 3 | 4 | import com.soulsspeedruns.organizer.savelist.Save; 5 | import com.soulsspeedruns.organizer.savelist.SaveListEntry; 6 | 7 | 8 | /** 9 | * Save Listener. 10 | *

11 | * Provides methods to listen for save changes. 12 | * 13 | * @author Kahmul (www.twitch.tv/kahmul78) 14 | * @date 1 Oct 2015 15 | */ 16 | public interface SaveListener 17 | { 18 | 19 | /** 20 | * Called when the selection in the SaveList changed. 21 | * 22 | * @param save the save that was selected 23 | */ 24 | public void entrySelected(SaveListEntry entry); 25 | 26 | 27 | /** 28 | * Called when a save is imported or a folder is created. 29 | * 30 | * @param entry the entry that was created 31 | */ 32 | public void entryCreated(SaveListEntry entry); 33 | 34 | 35 | /** 36 | * Called when an entry is renamed. 37 | * 38 | * @param entry the entry that was renamed 39 | */ 40 | public void entryRenamed(SaveListEntry entry); 41 | 42 | 43 | /** 44 | * Called when the load process of a save has been started. 45 | * 46 | * @param save the save that is loaded 47 | */ 48 | public void saveLoadStarted(Save save); 49 | 50 | 51 | /** 52 | * Called when the load process of save was finished. 53 | * 54 | * @param save the save that was loaded 55 | */ 56 | public void saveLoadFinished(Save save); 57 | 58 | 59 | /** 60 | * Called when the user switched the current gamefile from writable to read-only and vice versa. 61 | * 62 | * @param writable the writable state switched to 63 | */ 64 | public void gameFileWritableStateChanged(boolean writeable); 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/savelist/SaveListDragListener.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.savelist; 2 | 3 | 4 | import java.awt.dnd.DnDConstants; 5 | import java.awt.dnd.DragGestureEvent; 6 | import java.awt.dnd.DragGestureListener; 7 | import java.awt.dnd.DragSource; 8 | import java.awt.dnd.DragSourceDragEvent; 9 | import java.awt.dnd.DragSourceDropEvent; 10 | import java.awt.dnd.DragSourceEvent; 11 | import java.awt.dnd.DragSourceListener; 12 | 13 | 14 | /** 15 | * SaveListDragListener 16 | *

17 | * DragListener for SaveList. 18 | * 19 | * @author Kahmul (www.twitch.tv/kahmul78) 20 | * @date 27 May 2016 21 | */ 22 | public class SaveListDragListener implements DragSourceListener, DragGestureListener 23 | { 24 | 25 | private SaveList saveList; 26 | private DragSource dragSource; 27 | 28 | 29 | /** 30 | * Creates a new drag listener for a SaveList. 31 | */ 32 | public SaveListDragListener(SaveList saveList) 33 | { 34 | this.saveList = saveList; 35 | dragSource = new DragSource(); 36 | dragSource.createDefaultDragGestureRecognizer(saveList, DnDConstants.ACTION_MOVE, this); 37 | } 38 | 39 | 40 | @Override 41 | public void dragGestureRecognized(DragGestureEvent e) 42 | { 43 | if (saveList.getSelectedValue() != null) 44 | dragSource.startDrag(e, null, saveList.getSelectedValue(), this); 45 | } 46 | 47 | 48 | @Override 49 | public void dragDropEnd(DragSourceDropEvent e) 50 | { 51 | } 52 | 53 | 54 | @Override 55 | public void dragEnter(DragSourceDragEvent e) 56 | { 57 | } 58 | 59 | 60 | @Override 61 | public void dragExit(DragSourceEvent e) 62 | { 63 | } 64 | 65 | 66 | @Override 67 | public void dragOver(DragSourceDragEvent e) 68 | { 69 | } 70 | 71 | 72 | @Override 73 | public void dropActionChanged(DragSourceDragEvent e) 74 | { 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/savelist/RootFolder.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.savelist; 2 | 3 | 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | 8 | import javax.swing.JOptionPane; 9 | 10 | import com.soulsspeedruns.organizer.managers.GamesManager; 11 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 12 | 13 | 14 | /** 15 | * RootFolder class. 16 | *

17 | * Class representing the root folders of every profile. These folders don't have a parent. 18 | * 19 | * @author Kahmul (www.twitch.tv/kahmul78) 20 | * @date 8 Jul 2017 21 | */ 22 | public class RootFolder extends Folder 23 | { 24 | 25 | /** 26 | * 27 | * @param parent 28 | * @param file 29 | */ 30 | public RootFolder(File file) 31 | { 32 | super(null, file); 33 | } 34 | 35 | 36 | @Override 37 | public boolean rename(String newName) 38 | { 39 | File newFile = new File(GamesManager.getSelectedGame().getDirectory() + File.separator + newName); 40 | try 41 | { 42 | // if the same name is given, then only the file variable is supposed to be updated for a new parent 43 | if (!getFile().getName().equals((newName))) 44 | Files.move(getFile().toPath(), newFile.toPath()); 45 | } 46 | catch (IOException e) 47 | { 48 | JOptionPane.showMessageDialog(null, 49 | "Renaming the entries was not successful. They are possibly being accessed by another program.", "Warning", 50 | JOptionPane.WARNING_MESSAGE); 51 | return false; 52 | } 53 | setFile(newFile); 54 | for (SaveListEntry entry : getChildren()) 55 | { 56 | // call rename on all children with the same name to update the path with the new parent 57 | if (!entry.rename(entry.getName())) 58 | return false; 59 | } 60 | return true; 61 | } 62 | 63 | @Override 64 | public Folder getParent() { 65 | return this; 66 | } 67 | 68 | 69 | @Override 70 | public void delete() 71 | { 72 | OrganizerManager.deleteDirectory(getFile()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/components/HyperLink.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.components; 2 | 3 | import java.awt.Color; 4 | import java.awt.Cursor; 5 | import java.awt.Desktop; 6 | import java.awt.Insets; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | import java.net.URI; 10 | 11 | import javax.swing.JButton; 12 | import javax.swing.UIManager; 13 | import javax.swing.event.ChangeEvent; 14 | import javax.swing.event.ChangeListener; 15 | 16 | import com.github.weisj.darklaf.LafManager; 17 | import com.github.weisj.darklaf.theme.event.ThemeChangeEvent; 18 | import com.github.weisj.darklaf.theme.event.ThemeChangeListener; 19 | 20 | public class HyperLink extends JButton 21 | { 22 | 23 | public HyperLink(String text, String url) 24 | { 25 | super(text); 26 | 27 | setContentAreaFilled(false); 28 | setBorderPainted(false); 29 | setFocusPainted(false); 30 | setMargin(new Insets(0, 0, 0, 0)); 31 | 32 | setToolTipText(url); 33 | setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 34 | 35 | setForeground(new Color(UIManager.getColor("hyperlink").getRGB())); 36 | 37 | getModel().addChangeListener(new ChangeListener() { 38 | 39 | @Override 40 | public void stateChanged(ChangeEvent e) 41 | { 42 | setText(getModel().isRollover() ? "" + text + "" : text); 43 | } 44 | }); 45 | 46 | addActionListener(new ActionListener() { 47 | 48 | @Override 49 | public void actionPerformed(ActionEvent e) 50 | { 51 | try 52 | { 53 | Desktop.getDesktop().browse(new URI(url)); 54 | } 55 | catch (Exception ex) 56 | { 57 | } 58 | } 59 | }); 60 | 61 | LafManager.addThemeChangeListener(new ThemeChangeListener() { 62 | 63 | @Override 64 | public void themeInstalled(ThemeChangeEvent e) 65 | { 66 | setForeground(new Color(UIManager.getColor("hyperlink").getRGB())); 67 | } 68 | 69 | @Override 70 | public void themeChanged(ThemeChangeEvent e) 71 | { 72 | } 73 | }); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/theme/SoulsSpeedrunsTheme.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.theme; 2 | 3 | import java.util.Properties; 4 | 5 | import javax.swing.UIDefaults; 6 | 7 | import com.github.weisj.darklaf.properties.icons.IconResolver; 8 | import com.github.weisj.darklaf.theme.Theme; 9 | import com.github.weisj.darklaf.theme.info.PresetIconRule; 10 | import com.github.weisj.darklaf.theme.spec.ColorToneRule; 11 | 12 | 13 | public class SoulsSpeedrunsTheme extends Theme { 14 | 15 | @Override 16 | protected PresetIconRule getPresetIconRule() { 17 | return PresetIconRule.NONE; 18 | } 19 | 20 | @Override 21 | public String getPrefix() { 22 | return "soulsspeedruns"; 23 | } 24 | 25 | @Override 26 | public String getName() { 27 | return "SoulsSpeedruns"; 28 | } 29 | 30 | @Override 31 | protected String getResourcePath() { 32 | return ""; 33 | } 34 | 35 | @Override 36 | protected Class getLoaderClass() { 37 | return SoulsSpeedrunsTheme.class; 38 | } 39 | 40 | @Override 41 | public ColorToneRule getColorToneRule() { 42 | return ColorToneRule.DARK; 43 | } 44 | 45 | @Override 46 | public void customizeUIProperties(final Properties properties, final UIDefaults currentDefaults, 47 | final IconResolver iconResolver) { 48 | super.customizeUIProperties(properties, currentDefaults, iconResolver); 49 | loadCustomProperties("ui", properties, currentDefaults, iconResolver); 50 | } 51 | 52 | @Override 53 | public boolean supportsCustomAccentColor() { 54 | return true; 55 | } 56 | 57 | @Override 58 | public boolean supportsCustomSelectionColor() { 59 | return true; 60 | } 61 | 62 | @Override 63 | public void customizeIconTheme(final Properties properties, final UIDefaults currentDefaults, 64 | final IconResolver iconResolver) { 65 | // loadCustomProperties("icons_adjustments", properties, currentDefaults, iconResolver); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/components/SearchBar.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.components; 2 | 3 | 4 | import java.util.Timer; 5 | import java.util.TimerTask; 6 | 7 | import javax.swing.JTextField; 8 | import javax.swing.event.DocumentEvent; 9 | import javax.swing.event.DocumentListener; 10 | 11 | import com.github.weisj.darklaf.ui.text.DarkTextFieldUI; 12 | import com.github.weisj.darklaf.ui.text.DarkTextUI; 13 | import com.soulsspeedruns.organizer.managers.SavesManager; 14 | 15 | 16 | /** 17 | * Searchbar. 18 | *

19 | * Allows the user to send search requests to the SaveList. 20 | * 21 | * @author Kahmul (www.twitch.tv/kahmul78) 22 | * @date 18 May 2016 23 | */ 24 | public class SearchBar extends JTextField 25 | { 26 | 27 | public static final String DEFAULT_TEXT = "Search..."; 28 | private static final int SEARCH_DELAY = 500; 29 | 30 | private Timer searchDelayTimer; 31 | private TimerTask searchTask; 32 | 33 | 34 | /** 35 | * Creates a new searchbar. 36 | */ 37 | public SearchBar() 38 | { 39 | super(50); 40 | 41 | searchDelayTimer = new Timer(true); 42 | 43 | putClientProperty(DarkTextFieldUI.KEY_VARIANT, DarkTextFieldUI.VARIANT_SEARCH); 44 | putClientProperty(DarkTextFieldUI.KEY_SHOW_CLEAR, true); 45 | putClientProperty(DarkTextUI.KEY_DEFAULT_TEXT, DEFAULT_TEXT); 46 | 47 | getDocument().addDocumentListener(new DocumentListener() { 48 | 49 | @Override 50 | public void removeUpdate(DocumentEvent e) 51 | { 52 | rescheduleSearch(); 53 | } 54 | 55 | @Override 56 | public void insertUpdate(DocumentEvent e) 57 | { 58 | rescheduleSearch(); 59 | } 60 | 61 | @Override 62 | public void changedUpdate(DocumentEvent e) 63 | { 64 | rescheduleSearch(); 65 | } 66 | }); 67 | } 68 | 69 | private void rescheduleSearch() 70 | { 71 | if (searchTask != null) 72 | searchTask.cancel(); 73 | searchTask = new TimerTask() { 74 | 75 | @Override 76 | public void run() 77 | { 78 | SavesManager.fireSearchRequestedEvent(getText().trim()); 79 | } 80 | }; 81 | searchDelayTimer.schedule(searchTask, SEARCH_DELAY); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/update/NewReleasePanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.update; 2 | 3 | 4 | import java.awt.Font; 5 | 6 | import javax.swing.GroupLayout; 7 | import javax.swing.GroupLayout.Alignment; 8 | import javax.swing.JLabel; 9 | import javax.swing.JPanel; 10 | 11 | import com.soulsspeedruns.organizer.managers.VersionManager; 12 | 13 | 14 | /** 15 | * NewReleasePanel. 16 | *

17 | * Panel containing the info about a new release. 18 | * 19 | * @author Kahmul (www.twitch.tv/kahmul78) 20 | * @date 19 Jul 2017 21 | */ 22 | public class NewReleasePanel extends JPanel 23 | { 24 | 25 | /** 26 | * Creates a NewReleasePanel. 27 | */ 28 | protected NewReleasePanel() 29 | { 30 | GroupLayout layout = new GroupLayout(this); 31 | layout.setAutoCreateGaps(true); 32 | layout.setAutoCreateContainerGaps(true); 33 | 34 | JLabel newReleaseLabel = new JLabel("New Release Version:"); 35 | JLabel versionLabel = new JLabel(VersionManager.getLatestReleaseVersion()); 36 | JLabel changelogLabel = new JLabel("Changelog:"); 37 | 38 | String description = VersionManager.getLatestReleaseDescription().replaceAll("\\R", "
"); 39 | 40 | JLabel newReleaseDescriptionLabel = new JLabel(String.format("

%s
", 200, description)); 41 | 42 | newReleaseLabel.setFont(getFont().deriveFont(Font.BOLD)); 43 | changelogLabel.setFont(getFont().deriveFont(Font.BOLD)); 44 | 45 | // Horizontal 46 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 47 | 48 | hGroup.addGroup(layout.createParallelGroup().addComponent(newReleaseLabel).addComponent(changelogLabel) 49 | .addComponent(newReleaseDescriptionLabel)); 50 | hGroup.addGroup(layout.createParallelGroup().addComponent(versionLabel)); 51 | 52 | layout.setHorizontalGroup(hGroup); 53 | 54 | // Vertical 55 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 56 | 57 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(newReleaseLabel).addComponent(versionLabel)); 58 | 59 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(changelogLabel)); 60 | 61 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(newReleaseDescriptionLabel)); 62 | 63 | layout.setVerticalGroup(vGroup); 64 | 65 | setLayout(layout); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GamesConfigurationWindow.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import java.awt.Dialog; 5 | import java.awt.event.MouseAdapter; 6 | import java.awt.event.MouseEvent; 7 | import java.awt.event.WindowAdapter; 8 | import java.awt.event.WindowEvent; 9 | 10 | import javax.swing.JDialog; 11 | import javax.swing.SwingUtilities; 12 | 13 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 14 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 15 | import com.soulsspeedruns.organizer.managers.SettingsManager; 16 | 17 | 18 | /** 19 | * Window for configuring games. 20 | *

21 | * Allows the configuration of existing and creation of new games and their profiles. 22 | * 23 | * @author Kahmul (www.twitch.tv/kahmul78) 24 | * @date 27 Sep 2015 25 | */ 26 | public class GamesConfigurationWindow extends JDialog 27 | { 28 | 29 | private GamesConfigPane gamesPane; 30 | 31 | 32 | /** 33 | * Creates a new configuration window. 34 | */ 35 | public GamesConfigurationWindow() 36 | { 37 | super(OrganizerManager.getMainWindow(), "Games Configuration", Dialog.ModalityType.APPLICATION_MODAL); 38 | 39 | initLayout(); 40 | initProperties(); 41 | 42 | setVisible(true); 43 | } 44 | 45 | 46 | /** 47 | * Sets the properties of the window. 48 | */ 49 | private void initProperties() 50 | { 51 | pack(); 52 | setResizable(false); 53 | setLocationRelativeTo(OrganizerManager.getMainWindow()); 54 | setIconImage(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_MEDIUM)); 55 | setAlwaysOnTop(SettingsManager.isAlwaysOnTop()); 56 | 57 | addWindowListener(new WindowAdapter() 58 | { 59 | 60 | @Override 61 | public void windowOpened(WindowEvent e) 62 | { 63 | requestFocusInWindow(); 64 | SwingUtilities.invokeLater(() -> { 65 | SwingUtilities.updateComponentTreeUI(GamesConfigurationWindow.this); 66 | }); 67 | gamesPane.loadConfigPane(); 68 | } 69 | }); 70 | } 71 | 72 | 73 | /** 74 | * Adds all components to the layout. 75 | */ 76 | private void initLayout() 77 | { 78 | gamesPane = new GamesConfigPane(); 79 | 80 | gamesPane.addMouseListener(new MouseAdapter() 81 | { 82 | 83 | @Override 84 | public void mouseClicked(MouseEvent e) 85 | { 86 | requestFocusInWindow(); 87 | } 88 | }); 89 | 90 | add(gamesPane); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameListTransferHandler.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import javax.swing.TransferHandler; 5 | 6 | 7 | /** 8 | * Game List Transfer Handler 9 | *

10 | * A TransferHandler for GameList. 11 | * 12 | * @author Kahmul (www.twitch.tv/kahmul78) 13 | * @date 15 Jan 2024 14 | */ 15 | public class GameListTransferHandler extends TransferHandler 16 | { 17 | 18 | private GameList list; 19 | 20 | 21 | public GameListTransferHandler(GameList list) 22 | { 23 | this.list = list; 24 | } 25 | 26 | 27 | @Override 28 | public boolean canImport(TransferHandler.TransferSupport support) 29 | { 30 | if (!support.isDataFlavorSupported(GameListEntry.ENTRY_FLAVOR)) 31 | return false; 32 | try 33 | { 34 | int draggedIndex = (int) support.getTransferable().getTransferData(GameListEntry.ENTRY_FLAVOR); 35 | 36 | DropLocation dl = support.getDropLocation(); 37 | int dropIndex = list.getDropIndexByPoint(dl.getDropPoint()); 38 | 39 | if (isValidDropIndex(draggedIndex, dropIndex)) 40 | { 41 | list.setDropTargetIndex(dropIndex); 42 | return true; 43 | } 44 | 45 | list.clearDropTarget(); 46 | return false; 47 | } 48 | catch (Exception e) 49 | { 50 | e.printStackTrace(); 51 | return false; 52 | } 53 | } 54 | 55 | 56 | /** 57 | * Returns whether the given drop index is valid given the index of the dragged entry. 58 | * 59 | * @param draggedIndex the index of the dragged entry 60 | * @param dropIndex the index at which to drop the dragged entry 61 | * @return whether the drop index is valid 62 | */ 63 | private boolean isValidDropIndex(int draggedIndex, int dropIndex) 64 | { 65 | if (dropIndex > draggedIndex) 66 | return dropIndex - draggedIndex > 1; 67 | 68 | return draggedIndex - dropIndex > 0; 69 | } 70 | 71 | 72 | @Override 73 | public boolean importData(TransferHandler.TransferSupport support) 74 | { 75 | if (!canImport(support)) 76 | return false; 77 | try 78 | { 79 | int draggedIndex = (int) support.getTransferable().getTransferData(GameListEntry.ENTRY_FLAVOR); 80 | 81 | DropLocation dl = support.getDropLocation(); 82 | int dropIndex = list.getDropIndexByPoint(dl.getDropPoint()); 83 | 84 | list.moveIndexToNewIndex(draggedIndex, dropIndex); 85 | } 86 | catch (Exception e) 87 | { 88 | e.printStackTrace(); 89 | return false; 90 | } 91 | return true; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/config/SortingCategory.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main.config; 2 | 3 | 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.Paths; 8 | import java.nio.file.attribute.BasicFileAttributes; 9 | import java.nio.file.attribute.FileTime; 10 | 11 | import com.soulsspeedruns.organizer.savelist.SaveListEntry; 12 | 13 | 14 | /** 15 | * Sorting Category Enum. 16 | *

17 | * Enum representing the different sorting categories for the save list. 18 | * 19 | * @author Kahmul (www.twitch.tv/kahmul78) 20 | * @date 19 May 2016 21 | */ 22 | public enum SortingCategory 23 | { 24 | 25 | ALPHABET("Alphabet") 26 | { 27 | 28 | @Override 29 | public int compare(SaveListEntry s1, SaveListEntry s2) 30 | { 31 | return s1.getFile().getName().compareToIgnoreCase(s2.getName()); 32 | } 33 | }, 34 | DATE("Created") 35 | { 36 | 37 | @Override 38 | public int compare(SaveListEntry s1, SaveListEntry s2) 39 | { 40 | try 41 | { 42 | Path path = Paths.get(s1.getFile().getPath()); 43 | BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class); 44 | FileTime s1CreationTime = attributes.creationTime(); 45 | 46 | path = Paths.get(s2.getFile().getPath()); 47 | attributes = Files.readAttributes(path, BasicFileAttributes.class); 48 | FileTime s2CreationTime = attributes.creationTime(); 49 | return s2CreationTime.compareTo(s1CreationTime); 50 | } 51 | catch (IOException e) 52 | { 53 | e.printStackTrace(); 54 | } 55 | return 0; 56 | } 57 | }, 58 | READ_ONLY("Read Only") 59 | { 60 | 61 | @Override 62 | public int compare(SaveListEntry s1, SaveListEntry s2) 63 | { 64 | boolean s1ReadOnly = !s1.getFile().canWrite(); 65 | boolean s2ReadOnly = !s2.getFile().canWrite(); 66 | return s1ReadOnly ? (s2ReadOnly ? 0 : -1) : (s2ReadOnly ? 1 : 0); 67 | } 68 | }; 69 | 70 | private String caption; 71 | 72 | 73 | /** 74 | * Creates a new SortingCategory constant. 75 | * 76 | * @param caption the caption of the category 77 | */ 78 | private SortingCategory(String caption) 79 | { 80 | this.caption = caption; 81 | } 82 | 83 | 84 | /** 85 | * Returns the string representation of the category. 86 | * 87 | * @return the caption 88 | */ 89 | public String getCaption() 90 | { 91 | return caption; 92 | } 93 | 94 | 95 | public abstract int compare(SaveListEntry s1, SaveListEntry s2); 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/UndecoratedMessageDialog.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.BorderLayout; 5 | import java.awt.Color; 6 | import java.awt.Window; 7 | import java.awt.event.MouseEvent; 8 | import java.awt.event.MouseListener; 9 | import java.awt.event.WindowAdapter; 10 | import java.awt.event.WindowEvent; 11 | import java.awt.event.WindowListener; 12 | 13 | import javax.swing.JDialog; 14 | 15 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 16 | 17 | 18 | /** 19 | * UndecoratedMessageDialog. 20 | *

21 | * The undecorated dialog containing a given message panel, e.g. a successful load message. 22 | * 23 | * @author Kahmul (www.twitch.tv/kahmul78) 24 | * @date 15 Jul 2017 25 | */ 26 | public class UndecoratedMessageDialog extends JDialog implements MouseListener 27 | { 28 | 29 | private AbstractMessage message; 30 | 31 | 32 | /** 33 | * @param message the message to be shown 34 | */ 35 | protected UndecoratedMessageDialog(AbstractMessage message) 36 | { 37 | super(OrganizerManager.getMainWindow()); 38 | 39 | this.message = message; 40 | Window parentWindow = OrganizerManager.getMainWindow(); 41 | setSize(Math.min(400, parentWindow.getWidth() / 2), Math.min(100, parentWindow.getHeight() / 10)); 42 | setLocationRelativeTo(parentWindow); 43 | setUndecorated(true); 44 | setAutoRequestFocus(false); 45 | setBackground(new Color(0, 0, 0, 0)); 46 | setLayout(new BorderLayout()); 47 | add(message, BorderLayout.CENTER); 48 | 49 | WindowListener wl = new WindowAdapter() { 50 | 51 | public void windowDeactivated(WindowEvent e) 52 | { 53 | message.fadeOut(); 54 | } 55 | }; 56 | 57 | addWindowListener(wl); 58 | addMouseListener(this); 59 | } 60 | 61 | 62 | /** 63 | * Starts the fade-in of the message. 64 | */ 65 | protected void fadeIn() 66 | { 67 | setVisible(true); 68 | message.fadeIn(); 69 | } 70 | 71 | 72 | @Override 73 | public void mouseClicked(MouseEvent e) 74 | { 75 | } 76 | 77 | 78 | @Override 79 | public void mouseEntered(MouseEvent e) 80 | { 81 | 82 | } 83 | 84 | 85 | @Override 86 | public void mouseExited(MouseEvent e) 87 | { 88 | } 89 | 90 | 91 | @Override 92 | public void mousePressed(MouseEvent e) 93 | { 94 | message.fadeOut(); 95 | } 96 | 97 | 98 | @Override 99 | public void mouseReleased(MouseEvent e) 100 | { 101 | 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/theme/soulsspeedruns_ui.properties: -------------------------------------------------------------------------------- 1 | 2 | 3 | border = #FFFFFF 4 | 5 | Button.foreground = %soulsspeedrunsLinkColor 6 | Button.background = %soulsSpeedrunsComponentBackground 7 | Button.defaultBorderColor = %soulsspeedrunsListSelectionForeground 8 | Button.defaultFillColor = %soulsspeedrunsLinkColor 9 | Button.defaultFillColorRollOver = #fca94f 10 | Button.defaultFillColorClick = #fc9c36 11 | Button.disabledText = #495162 12 | 13 | ComboBox.foreground = %soulsspeedrunsLinkColor 14 | #ComboBox.selectionForeground = #212121 15 | ComboBox.selectionForeground = %soulsspeedrunsListSelectionForeground 16 | ComboBox.selectionBackground = %soulsspeedrunsListSelectionBackground 17 | 18 | Hyperlink.linkColor = %soulsspeedrunsLinkColor 19 | link.foreground = %soulsspeedrunsLinkColor 20 | 21 | TextField.background = %soulsSpeedrunsComponentBackground 22 | TextField.inactiveBackground = %soulsSpeedrunsComponentBackground 23 | 24 | CheckBox.activeFillColor = %soulsSpeedrunsComponentBackground 25 | CheckBox.foreground = %soulsspeedrunsLinkColor 26 | CheckBox.selectedFillColor = %soulsSpeedrunsComponentBackground 27 | CheckBox.selectionSelectedColor = %soulsspeedrunsLinkColor 28 | CheckBox.selectionFocusSelectedColor = %soulsspeedrunsLinkColor 29 | CheckBox.activeBorderColor = %soulsspeedrunsLinkColor 30 | CheckBox.selectedBorderColor = %soulsspeedrunsLinkColor 31 | 32 | List.selectionForeground = %soulsspeedrunsListSelectionForeground 33 | List.selectionBackground = %soulsspeedrunsListSelectionBackground 34 | List.foregroundSelected = %soulsspeedrunsListSelectionForeground 35 | List.backgroundSelected = %soulsspeedrunsListSelectionBackground 36 | List.backgroundSelectedNoFocus = %soulsspeedrunsListSelectionBackground 37 | List.foregroundSelectedNoFocus = %soulsspeedrunsListSelectionForeground 38 | 39 | TitledBorder.borderColor = #FFFFFF 40 | 41 | TabbedPane.selectedForeground = %soulsspeedrunsLinkColor 42 | TabbedPane.selectedBackground = %soulsspeedrunsListSelectionBackground 43 | TabbedPane.selectedHoverBackground = %soulsspeedrunsListSelectionBackground 44 | TabbedPane.hoverBackground = %soulsspeedrunsListSelectionBackground 45 | 46 | MenuItem.selectionBackground = %soulsspeedrunsListSelectionBackground 47 | MenuItem.selectionForeground = %soulsspeedrunsListSelectionForeground -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/config/GamesComboBox.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main.config; 2 | 3 | 4 | import java.awt.event.ItemEvent; 5 | import java.util.List; 6 | 7 | import javax.swing.JComboBox; 8 | 9 | import com.soulsspeedruns.organizer.games.Game; 10 | import com.soulsspeedruns.organizer.listeners.GameListener; 11 | import com.soulsspeedruns.organizer.managers.GamesManager; 12 | 13 | 14 | /** 15 | * GamesComboBox. 16 | *

17 | * ComboBox displaying Game objects. 18 | * 19 | * @author Kahmul (www.twitch.tv/kahmul78) 20 | * @date 29 Sep 2015 21 | */ 22 | public class GamesComboBox extends JComboBox implements GameListener 23 | { 24 | 25 | private boolean updateProfileComboBox = true; 26 | 27 | /** 28 | * Creates a new GamesComboBox. 29 | * 30 | * @param profilesCB the associated ProfilesComboBox 31 | */ 32 | public GamesComboBox(ProfilesComboBox profilesCB) 33 | { 34 | fillWith(Game.GAMES); 35 | 36 | setRenderer(new GamesComboBoxRenderer()); 37 | setPrototypeDisplayValue(Game.DARK_SOULS_II_SOTFS); 38 | addItemListener(event -> { 39 | if (event.getStateChange() == ItemEvent.SELECTED && updateProfileComboBox) 40 | { 41 | Game game = (Game) event.getItem(); 42 | GamesManager.switchToGame(game); 43 | profilesCB.setGame(game); 44 | } 45 | }); 46 | 47 | GamesManager.addGameListener(this); 48 | } 49 | 50 | 51 | /** 52 | * Fills the combobox with the given games. 53 | * 54 | * @param games the games to fill with 55 | */ 56 | public void fillWith(List games) 57 | { 58 | removeAllItems(); 59 | if (games != null && games.size() > 0) 60 | { 61 | for (int i = 0; i < games.size(); i++) 62 | { 63 | addItem(games.get(i)); 64 | if (GamesManager.getSelectedGame() == games.get(i)) 65 | { 66 | setSelectedItem(games.get(i)); 67 | } 68 | } 69 | } 70 | } 71 | 72 | 73 | @Override 74 | public void gameCreated(Game game) 75 | { 76 | addItem(game); 77 | } 78 | 79 | 80 | @Override 81 | public void gameDeleted(Game game) 82 | { 83 | removeItem(game); 84 | if(GamesManager.getSelectedGame() == game) 85 | setSelectedIndex(Math.max(0, getSelectedIndex() - 1)); 86 | } 87 | 88 | 89 | @Override 90 | public void gameEdited(Game game) 91 | { 92 | 93 | } 94 | 95 | @Override 96 | public void gameMoved(Game game, int newIndex) 97 | { 98 | Game selectedGame = (Game) getSelectedItem(); 99 | 100 | updateProfileComboBox = false; 101 | 102 | removeItem(game); 103 | insertItemAt(game, newIndex); 104 | 105 | setSelectedItem(selectedGame); 106 | 107 | updateProfileComboBox = true; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/settings/HotkeysSettingsPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.settings; 2 | 3 | 4 | import java.awt.Component; 5 | 6 | import javax.swing.BorderFactory; 7 | import javax.swing.Box; 8 | import javax.swing.GroupLayout; 9 | import javax.swing.GroupLayout.Alignment; 10 | import javax.swing.JLabel; 11 | import javax.swing.JPanel; 12 | import javax.swing.border.EtchedBorder; 13 | import javax.swing.border.TitledBorder; 14 | 15 | import com.soulsspeedruns.organizer.hotkeys.GlobalHotkey; 16 | 17 | 18 | /** 19 | * HotkeysSettingsPanel. 20 | *

21 | * Contains the textfields that allow the user to change the global hotkeys. 22 | * 23 | * @author Kahmul (www.twitch.tv/kahmul78) 24 | * @date 3 Jun 2016 25 | */ 26 | public class HotkeysSettingsPanel extends JPanel 27 | { 28 | 29 | private HotkeyTextField[] fields; 30 | 31 | 32 | /** 33 | * Creates a new hotkeys settings panel. 34 | */ 35 | protected HotkeysSettingsPanel() 36 | { 37 | GroupLayout layout = new GroupLayout(this); 38 | layout.setAutoCreateGaps(true); 39 | layout.setAutoCreateContainerGaps(true); 40 | 41 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 42 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 43 | 44 | Component glue = Box.createHorizontalStrut(50); 45 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(glue)); 46 | 47 | GroupLayout.ParallelGroup hLabelGroup = layout.createParallelGroup(); 48 | GroupLayout.ParallelGroup hFieldGroup = layout.createParallelGroup(); 49 | 50 | GlobalHotkey[] hotkeys = GlobalHotkey.values(); 51 | fields = new HotkeyTextField[hotkeys.length]; 52 | 53 | for (int i = 0; i < hotkeys.length; i++) 54 | { 55 | JLabel label = new JLabel(hotkeys[i].getCaption()); 56 | fields[i] = new HotkeyTextField(hotkeys[i]); 57 | 58 | hLabelGroup.addComponent(label); 59 | hFieldGroup.addComponent(fields[i]); 60 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(label).addComponent(fields[i])); 61 | } 62 | 63 | hGroup.addGroup(hLabelGroup); 64 | hGroup.addGroup(layout.createParallelGroup().addComponent(glue)); 65 | hGroup.addGroup(hFieldGroup); 66 | 67 | layout.setHorizontalGroup(hGroup); 68 | layout.setVerticalGroup(vGroup); 69 | 70 | setLayout(layout); 71 | TitledBorder border = BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), "Global Hotkeys"); 72 | setBorder(border); 73 | } 74 | 75 | 76 | /** 77 | * Applies the changes to the hotkey shortcut. 78 | */ 79 | protected void applyChanges() 80 | { 81 | for (HotkeyTextField hotkeyTextField : fields) 82 | { 83 | hotkeyTextField.saveChangesToHotkey(); 84 | } 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/Profile.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games; 2 | 3 | 4 | import java.io.File; 5 | import java.util.Collections; 6 | 7 | import com.soulsspeedruns.organizer.managers.GamesManager; 8 | import com.soulsspeedruns.organizer.savelist.Folder; 9 | import com.soulsspeedruns.organizer.savelist.RootFolder; 10 | 11 | 12 | /** 13 | * Profile class. 14 | *

15 | * Class representing the profiles for each game. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 27 Sep 2015 19 | */ 20 | public class Profile implements Comparable 21 | { 22 | 23 | private final Game game; 24 | private RootFolder root; 25 | 26 | 27 | /** 28 | * Creates a new Profile object with the given name and game. 29 | * 30 | * @param name the name of the profile 31 | * @param game the game that this profile belongs to 32 | */ 33 | public Profile(String name, Game game) 34 | { 35 | this.game = game; 36 | if (name != null && name.length() > 0) 37 | root = new RootFolder(new File(game.getDirectory() + File.separator + name)); 38 | } 39 | 40 | 41 | /** 42 | * @return the name of this profile. 43 | */ 44 | public String getName() 45 | { 46 | if (root != null) 47 | return root.getName(); 48 | return ""; 49 | } 50 | 51 | 52 | /** 53 | * @return the game this profile is used for. 54 | */ 55 | public Game getGame() 56 | { 57 | return game; 58 | } 59 | 60 | 61 | /** 62 | * The root folder containing all folders and savestates for this profile. 63 | * 64 | * @return the root folder 65 | */ 66 | public Folder getRoot() 67 | { 68 | return root; 69 | } 70 | 71 | 72 | /** 73 | * Renames the profile to the given name. 74 | * 75 | * @param name the new name of this profile 76 | */ 77 | public void rename(String name) 78 | { 79 | boolean updateSelectedProfile = this.equals(GamesManager.getSelectedProfile()); 80 | root.rename(name); 81 | if (updateSelectedProfile) 82 | GamesManager.switchToProfile(this); // update the name of the stored selected profile 83 | Collections.sort(game.getProfiles()); 84 | } 85 | 86 | 87 | /** 88 | * Deletes this profile and all of its associated savefiles and folders. 89 | */ 90 | public void delete() 91 | { 92 | game.removeProfile(this); 93 | root.delete(); 94 | } 95 | 96 | /** 97 | * Refreshes the profile to pick up any new files. 98 | */ 99 | public void refreshProfile() 100 | { 101 | if(root != null) 102 | root = new RootFolder(new File(game.getDirectory() + File.separator + root.getName())); 103 | } 104 | 105 | 106 | @Override 107 | public int compareTo(Profile profile) 108 | { 109 | return getName().compareToIgnoreCase(profile.getName()); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/update/NewReleaseWindow.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.update; 2 | 3 | 4 | import java.awt.Dialog; 5 | import java.awt.FlowLayout; 6 | import java.awt.GridBagConstraints; 7 | import java.awt.GridBagLayout; 8 | 9 | import javax.swing.BorderFactory; 10 | import javax.swing.ImageIcon; 11 | import javax.swing.JButton; 12 | import javax.swing.JDialog; 13 | import javax.swing.JLabel; 14 | import javax.swing.JPanel; 15 | 16 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 17 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 18 | import com.soulsspeedruns.organizer.managers.SettingsManager; 19 | import com.soulsspeedruns.organizer.managers.VersionManager; 20 | 21 | 22 | /** 23 | * NewReleaseWindow. 24 | *

25 | * Window giving info about a new release, if there is any, as well as a link to the download. 26 | * 27 | * @author Kahmul (www.twitch.tv/kahmul78) 28 | * @date 19 Jul 2017 29 | */ 30 | public class NewReleaseWindow extends JDialog 31 | { 32 | 33 | /** 34 | * Creates a new AboutWindow. 35 | */ 36 | public NewReleaseWindow() 37 | { 38 | super(null, "New Release Available!", Dialog.ModalityType.APPLICATION_MODAL); 39 | 40 | initLayout(); 41 | initProperties(); 42 | 43 | setVisible(true); 44 | requestFocus(); 45 | } 46 | 47 | 48 | /** 49 | * 50 | */ 51 | private void initProperties() 52 | { 53 | setResizable(false); 54 | setIconImage(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_MEDIUM)); 55 | setLocationRelativeTo(OrganizerManager.getMainWindow()); 56 | setAlwaysOnTop(SettingsManager.isAlwaysOnTop()); 57 | } 58 | 59 | 60 | /** 61 | * 62 | */ 63 | private void initLayout() 64 | { 65 | JPanel guiPanel = new JPanel(); 66 | guiPanel.setLayout(new GridBagLayout()); 67 | guiPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 68 | 69 | JPanel contentPanel = new JPanel(); 70 | contentPanel.setLayout(new FlowLayout(FlowLayout.LEADING, 50, 0)); 71 | contentPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 20, 5)); 72 | 73 | contentPanel.add(new NewReleasePanel()); 74 | contentPanel.add(new JLabel(new ImageIcon(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_LARGE)))); 75 | 76 | GridBagConstraints constraints = new GridBagConstraints(); 77 | guiPanel.add(contentPanel, constraints); 78 | constraints.gridy = 1; 79 | guiPanel.add(createNewDownloadButton(), constraints); 80 | 81 | add(guiPanel); 82 | pack(); 83 | } 84 | 85 | 86 | private JButton createNewDownloadButton() 87 | { 88 | JButton downloadButton = new JButton("Go to Releases"); 89 | downloadButton.addActionListener(e -> { 90 | OrganizerManager.openLatestReleasePage(); 91 | setVisible(false); 92 | }); 93 | downloadButton.setToolTipText(VersionManager.getLatestReleaseURL()); 94 | return downloadButton; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/savelist/Save.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.savelist; 2 | 3 | 4 | import java.awt.Color; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | 9 | import javax.swing.BorderFactory; 10 | import javax.swing.Icon; 11 | import javax.swing.JLabel; 12 | import javax.swing.JList; 13 | import javax.swing.JOptionPane; 14 | 15 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 16 | import com.soulsspeedruns.organizer.managers.SavesManager; 17 | 18 | import jiconfont.icons.Iconic; 19 | import jiconfont.swing.IconFontSwing; 20 | 21 | 22 | /** 23 | * Savestate class. 24 | *

25 | * Class representing the savestates and folders. 26 | * 27 | * @author Kahmul (www.twitch.tv/kahmul78) 28 | * @date 26 Sep 2015 29 | */ 30 | public class Save extends SaveListEntry 31 | { 32 | 33 | private static final Icon ICON_FILE_DOES_NOT_EXIST = IconFontSwing.buildIcon(Iconic.CHECK, 13, Color.RED); 34 | 35 | /** 36 | * Creates a new Save instance. 37 | * 38 | * @param parent the parent folder 39 | * @param file the associated file 40 | */ 41 | public Save(Folder parent, File file) 42 | { 43 | super(parent, file); 44 | } 45 | 46 | 47 | @Override 48 | public int compareTo(SaveListEntry entry) 49 | { 50 | if (entry instanceof Folder) 51 | return 1; 52 | return SavesManager.getSelectedSortingCategory().compare(this, entry); 53 | } 54 | 55 | 56 | @Override 57 | public boolean rename(String newName) 58 | { 59 | File newFile = new File(getParent().getFile() + File.separator + newName); 60 | try 61 | { 62 | // if the same name is given, then only the file variable is supposed to be updated for a new parent 63 | if (!getFile().getName().equals((newName))) 64 | Files.move(getFile().toPath(), newFile.toPath()); 65 | } 66 | catch (IOException e) 67 | { 68 | JOptionPane.showMessageDialog(null, 69 | "Renaming the entries was not successful. They are possibly being accessed by another program.", "Warning", 70 | JOptionPane.WARNING_MESSAGE); 71 | return false; 72 | } 73 | setFile(newFile); 74 | return true; 75 | } 76 | 77 | 78 | @Override 79 | public boolean canBeRenamed() 80 | { 81 | return getFile().canWrite(); 82 | } 83 | 84 | 85 | @Override 86 | public void render(JList list, int index, JLabel label) 87 | { 88 | label.setText(getFile().getName()); 89 | label.setBorder(BorderFactory.createEmptyBorder(1, 3 + getIndent(), 0, 1)); 90 | if(isMarkedForCut()) 91 | label.setForeground(Color.GRAY); 92 | if (!getFile().canWrite()) 93 | label.setIcon(IconsAndFontsManager.getReadOnlyIcon(IconsAndFontsManager.ICON_SIZE_SMALL, false)); 94 | if (!getFile().exists()) 95 | { 96 | label.setIcon(ICON_FILE_DOES_NOT_EXIST); 97 | label.setForeground(Color.RED); 98 | label.setToolTipText("File no longer exists!"); 99 | } 100 | } 101 | 102 | 103 | @Override 104 | public void delete() 105 | { 106 | getParent().removeChild(this); 107 | getFile().delete(); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/settings/ButtonsSettingsPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.settings; 2 | 3 | 4 | import java.awt.Component; 5 | 6 | import javax.swing.Box; 7 | import javax.swing.GroupLayout; 8 | import javax.swing.GroupLayout.Alignment; 9 | import javax.swing.ImageIcon; 10 | import javax.swing.JButton; 11 | import javax.swing.JPanel; 12 | 13 | import com.soulsspeedruns.organizer.about.AboutWindow; 14 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 15 | 16 | 17 | /** 18 | * ButtonSettingsPanel. 19 | *

20 | * Contains the About button. 21 | * 22 | * @author Kahmul (www.twitch.tv/kahmul78) 23 | * @date 4 Jun 2016 24 | */ 25 | public class ButtonsSettingsPanel extends JPanel 26 | { 27 | 28 | /** 29 | * Creates a new buttons settings panel. 30 | * 31 | * @param window the settings window 32 | */ 33 | protected ButtonsSettingsPanel(SettingsWindow window) 34 | { 35 | GroupLayout layout = new GroupLayout(this); 36 | layout.setAutoCreateGaps(true); 37 | layout.setAutoCreateContainerGaps(true); 38 | 39 | 40 | JButton aboutButton = createAboutButton(); 41 | // JButton saveButton = createSaveButton(window); 42 | // JButton cancelButton = createCancelButton(window); 43 | 44 | Component glue = Box.createHorizontalStrut(238); 45 | 46 | // Horizontal 47 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 48 | 49 | hGroup.addComponent(glue).addComponent(aboutButton);//.addComponent(saveButton).addComponent(cancelButton); 50 | 51 | layout.setHorizontalGroup(hGroup); 52 | 53 | // Vertical 54 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 55 | 56 | vGroup.addGroup( 57 | layout.createParallelGroup(Alignment.TRAILING).addComponent(glue).addComponent(aboutButton));//.addComponent(saveButton).addComponent(cancelButton)); 58 | 59 | layout.setVerticalGroup(vGroup); 60 | 61 | setLayout(layout); 62 | } 63 | 64 | private JButton createAboutButton() 65 | { 66 | JButton aboutButton = new JButton("About", new ImageIcon(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_SMALL))); 67 | aboutButton.addActionListener(event -> new AboutWindow()); 68 | return aboutButton; 69 | } 70 | 71 | 72 | /** 73 | * Creates the save button. 74 | * 75 | * @param window the settings window 76 | * @return the save button 77 | */ 78 | // private JButton createSaveButton(SettingsWindow window) 79 | // { 80 | // JButton saveButton = new JButton("Save"); 81 | // saveButton.addActionListener(event -> { 82 | // window.saveSettings(); 83 | // window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING)); 84 | // }); 85 | // return saveButton; 86 | // } 87 | 88 | 89 | /** 90 | * Creates the cancel button. 91 | * 92 | * @param window the settings window 93 | * @return the cancel button 94 | */ 95 | // private JButton createCancelButton(SettingsWindow window) 96 | // { 97 | // JButton cancelButton = new JButton("Cancel"); 98 | // cancelButton.addActionListener(event -> window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING))); 99 | // return cancelButton; 100 | // } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/settings/SettingsWindow.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.settings; 2 | 3 | 4 | import java.awt.Dialog; 5 | import java.awt.event.MouseAdapter; 6 | import java.awt.event.MouseEvent; 7 | import java.awt.event.WindowAdapter; 8 | import java.awt.event.WindowEvent; 9 | 10 | import javax.swing.BorderFactory; 11 | import javax.swing.BoxLayout; 12 | import javax.swing.JDialog; 13 | import javax.swing.JPanel; 14 | import javax.swing.SwingUtilities; 15 | 16 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 17 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 18 | import com.soulsspeedruns.organizer.managers.SettingsManager; 19 | 20 | 21 | /** 22 | * SettingsWindow. 23 | *

24 | * Allows the user to configure various settings. 25 | * 26 | * @author Kahmul (www.twitch.tv/kahmul78) 27 | * @date 3 Jun 2016 28 | */ 29 | public class SettingsWindow extends JDialog 30 | { 31 | 32 | private GeneralSettingsPanel generalSettingsPanel; 33 | private HotkeysSettingsPanel hotkeysSettingsPanel; 34 | 35 | 36 | /** 37 | * Creates a new SettingsWindow. 38 | */ 39 | public SettingsWindow() 40 | { 41 | super(OrganizerManager.getMainWindow(), "Settings", Dialog.ModalityType.APPLICATION_MODAL); 42 | 43 | initLayout(); 44 | initProperties(); 45 | 46 | setVisible(true); 47 | } 48 | 49 | 50 | /** 51 | * Inits the properties. 52 | */ 53 | private void initProperties() 54 | { 55 | pack(); 56 | setResizable(false); 57 | SettingsManager.getKeyboardHook().unregisterHook(); 58 | setIconImage(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_MEDIUM)); 59 | setLocationRelativeTo(OrganizerManager.getMainWindow()); 60 | setAlwaysOnTop(SettingsManager.isAlwaysOnTop()); 61 | addMouseListener(new MouseAdapter() { 62 | 63 | @Override 64 | public void mouseClicked(MouseEvent e) 65 | { 66 | requestFocusInWindow(); 67 | } 68 | }); 69 | addWindowListener(new WindowAdapter() { 70 | 71 | @Override 72 | public void windowOpened(WindowEvent e) 73 | { 74 | requestFocusInWindow(); 75 | SwingUtilities.invokeLater(() -> { 76 | SwingUtilities.updateComponentTreeUI(SettingsWindow.this); 77 | }); 78 | } 79 | 80 | @Override 81 | public void windowClosing(WindowEvent e) 82 | { 83 | saveSettings(); 84 | SettingsManager.getKeyboardHook().registerHook(); 85 | } 86 | }); 87 | } 88 | 89 | 90 | /** 91 | * Inits the layout. 92 | */ 93 | private void initLayout() 94 | { 95 | JPanel guiPanel = new JPanel(); 96 | guiPanel.setLayout(new BoxLayout(guiPanel, BoxLayout.PAGE_AXIS)); 97 | guiPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 98 | 99 | generalSettingsPanel = new GeneralSettingsPanel(); 100 | hotkeysSettingsPanel = new HotkeysSettingsPanel(); 101 | guiPanel.add(generalSettingsPanel); 102 | guiPanel.add(hotkeysSettingsPanel); 103 | guiPanel.add(new ButtonsSettingsPanel(this)); 104 | 105 | add(guiPanel); 106 | } 107 | 108 | 109 | /** 110 | * Saves the changes to the settings. 111 | */ 112 | protected void saveSettings() 113 | { 114 | generalSettingsPanel.applyChanges(); 115 | hotkeysSettingsPanel.applyChanges(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/about/AboutPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.about; 2 | 3 | 4 | import java.awt.Color; 5 | 6 | import javax.swing.GroupLayout; 7 | import javax.swing.GroupLayout.Alignment; 8 | import javax.swing.JLabel; 9 | import javax.swing.JPanel; 10 | import javax.swing.SwingConstants; 11 | 12 | import com.soulsspeedruns.organizer.components.HyperLink; 13 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 14 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 15 | import com.soulsspeedruns.organizer.managers.VersionManager; 16 | 17 | import jiconfont.icons.Elusive; 18 | import jiconfont.swing.IconFontSwing; 19 | 20 | 21 | /** 22 | * AboutPanel. 23 | *

24 | * Panel containing information about the program. 25 | * 26 | * @author Kahmul (www.twitch.tv/kahmul78) 27 | * @date 5 Jun 2016 28 | */ 29 | public class AboutPanel extends JPanel 30 | { 31 | 32 | /** 33 | * Creates a new AboutPanel. 34 | */ 35 | protected AboutPanel() 36 | { 37 | GroupLayout layout = new GroupLayout(this); 38 | layout.setAutoCreateGaps(true); 39 | layout.setAutoCreateContainerGaps(true); 40 | 41 | JLabel versionLabel = new JLabel("Version:"); 42 | versionLabel.setHorizontalAlignment(SwingConstants.RIGHT); 43 | JLabel versionNumberLabel = new JLabel(VersionManager.getVersion()); 44 | 45 | JLabel developerLabel = new JLabel("Developed by:"); 46 | developerLabel.setHorizontalAlignment(SwingConstants.RIGHT); 47 | 48 | JLabel discordLabel = createDiscordLabel(); 49 | HyperLink devLink = createDevLink(); 50 | HyperLink githubLink = createGitHubLink(); 51 | 52 | // Horizontal 53 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 54 | 55 | hGroup.addGroup(layout.createParallelGroup(Alignment.TRAILING).addComponent(versionLabel).addComponent(versionNumberLabel) 56 | .addComponent(developerLabel).addComponent(discordLabel).addComponent(devLink).addComponent(githubLink)); 57 | 58 | layout.setHorizontalGroup(hGroup); 59 | 60 | // Vertical 61 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 62 | 63 | vGroup.addComponent(versionLabel).addComponent(versionNumberLabel).addComponent(developerLabel).addComponent(discordLabel) 64 | .addComponent(devLink).addComponent(githubLink); 65 | 66 | layout.setVerticalGroup(vGroup); 67 | 68 | setLayout(layout); 69 | } 70 | 71 | 72 | private JLabel createDiscordLabel() 73 | { 74 | JLabel discordLabel = new JLabel("Kahmul"); 75 | discordLabel.setIcon(IconsAndFontsManager.getDiscordIcon()); 76 | 77 | return discordLabel; 78 | } 79 | 80 | 81 | private HyperLink createDevLink() 82 | { 83 | HyperLink devLink = new HyperLink("Kahmul78", OrganizerManager.TWITTER_URL); 84 | devLink.setHorizontalAlignment(SwingConstants.RIGHT); 85 | devLink.setIcon(IconFontSwing.buildIcon(Elusive.TWITTER, 20, new Color(64, 153, 255))); 86 | 87 | return devLink; 88 | } 89 | 90 | 91 | private HyperLink createGitHubLink() 92 | { 93 | HyperLink githubLink = new HyperLink("GitHub Repository", OrganizerManager.GITHUB_REPO_URL); 94 | githubLink.setHorizontalAlignment(SwingConstants.RIGHT); 95 | githubLink.setIcon(IconFontSwing.buildIcon(Elusive.GITHUB, 20)); 96 | 97 | return githubLink; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/config/ProfilesComboBox.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main.config; 2 | 3 | 4 | import java.awt.event.ItemEvent; 5 | import java.util.List; 6 | 7 | import javax.swing.JComboBox; 8 | 9 | import com.soulsspeedruns.organizer.games.Game; 10 | import com.soulsspeedruns.organizer.games.Profile; 11 | import com.soulsspeedruns.organizer.listeners.ProfileListener; 12 | import com.soulsspeedruns.organizer.managers.GamesManager; 13 | 14 | 15 | /** 16 | * ProfilesComboBox. 17 | *

18 | * ComboBox displaying Profile objects. 19 | * 20 | * @author Kahmul (www.twitch.tv/kahmul78) 21 | * @date 29 Sep 2015 22 | */ 23 | public class ProfilesComboBox extends JComboBox implements ProfileListener 24 | { 25 | 26 | /** 27 | * Creates a new ProfilesComboBox. 28 | * 29 | * @param game the game associated with this combobox 30 | */ 31 | public ProfilesComboBox(Game game) 32 | { 33 | fillWith(game); 34 | 35 | GamesManager.addProfileListener(this); 36 | setRenderer(new ProfilesComboBoxRenderer()); 37 | addItemListener(event -> { 38 | if (event.getStateChange() == ItemEvent.SELECTED) 39 | { 40 | GamesManager.switchToProfile((Profile) event.getItem()); 41 | } 42 | }); 43 | } 44 | 45 | 46 | /** 47 | * Fills the combobox with the profiles of the given game. 48 | * 49 | * @param game the game to get the profiles from 50 | */ 51 | private void fillWith(Game game) 52 | { 53 | removeAllItems(); 54 | List profiles = game.getProfiles(); 55 | for (Profile profile : profiles) 56 | { 57 | addItem(profile); 58 | if (GamesManager.getSelectedProfile().equals(profile)) 59 | setSelectedItem(profile); 60 | } 61 | if (profiles.size() == 0) 62 | GamesManager.switchToProfile(new Profile("", game)); 63 | } 64 | 65 | 66 | /** 67 | * Sets the game to retrieve the profiles from. 68 | * 69 | * @param game the game to retrieve the profiles from 70 | */ 71 | public void setGame(Game game) 72 | { 73 | if (game != null) 74 | fillWith(game); 75 | } 76 | 77 | 78 | @Override 79 | public void profileDeleted(Profile profile) 80 | { 81 | if (GamesManager.getSelectedGame().equals(profile.getGame())) 82 | { 83 | removeItem(profile); 84 | // if the currently chosen profile is deleted, another one gets auto selected, without the Manager being updated. So 85 | // switchToProfile() is called to update it. This also indirectly calls repaint(). 86 | GamesManager.switchToProfile((Profile) getSelectedItem()); 87 | } 88 | } 89 | 90 | 91 | @Override 92 | public void profileCreated(Profile profile) 93 | { 94 | if (GamesManager.getSelectedGame().equals(profile.getGame())) 95 | fillWith(GamesManager.getSelectedGame()); 96 | } 97 | 98 | 99 | @Override 100 | public void profileDirectoryChanged(Game game) 101 | { 102 | if (GamesManager.getSelectedGame().equals(game)) 103 | fillWith(GamesManager.getSelectedGame()); 104 | } 105 | 106 | 107 | @Override 108 | public void changedToProfile(Profile profile) 109 | { 110 | // updates the combobox UI when a profile is edited that is currently selected in the organizer, as changing the name 111 | // requires calling switchToProfile() to update the stored selected profile. As a result, this method is called as well, 112 | // so the combobox is repainted. 113 | repaint(); 114 | } 115 | 116 | 117 | @Override 118 | public void changedToGame(Game game) 119 | { 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/settings/HotkeyTextField.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.settings; 2 | 3 | 4 | import java.awt.event.FocusEvent; 5 | import java.awt.event.FocusListener; 6 | import java.awt.event.KeyEvent; 7 | import java.awt.event.KeyListener; 8 | 9 | import javax.swing.JTextField; 10 | 11 | import com.soulsspeedruns.organizer.hotkeys.GlobalHotkey; 12 | 13 | 14 | /** 15 | * HotkeyTextField. 16 | *

17 | * Textfield for changing the global hotkey for an action. 18 | * 19 | * @author Kahmul (www.twitch.tv/kahmul78) 20 | * @date 4 Jun 2016 21 | */ 22 | public class HotkeyTextField extends JTextField implements FocusListener, KeyListener 23 | { 24 | 25 | private static final String WAITING_FOR_INPUT_TEXT = "Set Hotkey..."; 26 | 27 | private String currentKey; 28 | private GlobalHotkey hotkey; 29 | 30 | 31 | /** 32 | * Creates a textfield for a hotkey. 33 | * 34 | * @param hotkey the hotkey to create this textfield for 35 | */ 36 | protected HotkeyTextField(GlobalHotkey hotkey) 37 | { 38 | super(hotkey.getKeyCode()); 39 | this.hotkey = hotkey; 40 | currentKey = getText(); 41 | 42 | addFocusListener(this); 43 | addKeyListener(this); 44 | setHorizontalAlignment(JTextField.CENTER); 45 | setEditable(false); 46 | } 47 | 48 | 49 | /** 50 | * @return the hotkey 51 | */ 52 | public GlobalHotkey getHotkey() 53 | { 54 | return hotkey; 55 | } 56 | 57 | 58 | /** 59 | * Saves the changed hotkey shortcut to the associated hotkey. 60 | */ 61 | protected void saveChangesToHotkey() 62 | { 63 | hotkey.setKeyCode(currentKey); 64 | } 65 | 66 | 67 | @Override 68 | public void focusGained(FocusEvent e) 69 | { 70 | setText(WAITING_FOR_INPUT_TEXT); 71 | } 72 | 73 | 74 | @Override 75 | public void focusLost(FocusEvent e) 76 | { 77 | setText(currentKey); 78 | } 79 | 80 | 81 | @Override 82 | public void keyTyped(KeyEvent e) 83 | { 84 | } 85 | 86 | 87 | @Override 88 | public void keyPressed(KeyEvent e) 89 | { 90 | int keyCode = e.getKeyCode(); 91 | if (keyCode == KeyEvent.VK_ESCAPE) 92 | { 93 | currentKey = "None"; 94 | setText("None"); 95 | getParent().requestFocusInWindow(); 96 | return; 97 | 98 | } 99 | if (keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_CONTROL || keyCode == KeyEvent.VK_ALT) 100 | return; 101 | updateKey(e); 102 | } 103 | 104 | 105 | @Override 106 | public void keyReleased(KeyEvent e) 107 | { 108 | int keyCode = e.getKeyCode(); 109 | if (keyCode == KeyEvent.VK_SHIFT || keyCode == KeyEvent.VK_CONTROL || keyCode == KeyEvent.VK_ALT) 110 | { 111 | // if only modifier keys have been pressed so far and one is released, use them for the keycode 112 | if (WAITING_FOR_INPUT_TEXT.equals(getText())) 113 | updateKey(e); 114 | } 115 | getParent().requestFocusInWindow(); 116 | } 117 | 118 | 119 | /** 120 | * Updates the hotkey of this hotkey field with the given KeyEvent. 121 | * 122 | * @param e the KeyEvent 123 | */ 124 | private void updateKey(KeyEvent e) 125 | { 126 | String modifiers = KeyEvent.getModifiersExText(e.getModifiersEx()); 127 | modifiers = modifiers.replaceAll("\\+", " \\+ "); 128 | modifiers = modifiers.length() > 0 ? modifiers + " + " : modifiers; 129 | 130 | String keyText = KeyEvent.getKeyText(e.getKeyCode()); 131 | keyText = keyText.replaceAll("NumPad-", "NumPad "); 132 | 133 | String text = modifiers + keyText; 134 | setText(text); 135 | currentKey = text; 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/about/AboutWindow.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.about; 2 | 3 | 4 | import java.awt.Dialog; 5 | import java.awt.FlowLayout; 6 | import java.awt.event.MouseAdapter; 7 | import java.awt.event.MouseEvent; 8 | import java.awt.event.WindowAdapter; 9 | import java.awt.event.WindowEvent; 10 | 11 | import javax.swing.BorderFactory; 12 | import javax.swing.ImageIcon; 13 | import javax.swing.JDialog; 14 | import javax.swing.JLabel; 15 | import javax.swing.JPanel; 16 | import javax.swing.SwingUtilities; 17 | 18 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 19 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 20 | import com.soulsspeedruns.organizer.managers.SettingsManager; 21 | 22 | 23 | /** 24 | * AboutWindow. 25 | *

26 | * Small window showing links to Twitter and GitHub, as well as the current version. 27 | * 28 | * @author Kahmul (www.twitch.tv/kahmul78) 29 | * @date 4 Jun 2016 30 | */ 31 | public class AboutWindow extends JDialog 32 | { 33 | 34 | 35 | /** 36 | * Creates a new AboutWindow. 37 | */ 38 | public AboutWindow() 39 | { 40 | super(null, "About SoulsSpeedruns - Save Organizer", Dialog.ModalityType.APPLICATION_MODAL); 41 | 42 | initLayout(); 43 | initProperties(); 44 | 45 | setVisible(true); 46 | } 47 | 48 | 49 | /** 50 | * 51 | */ 52 | private void initProperties() 53 | { 54 | pack(); 55 | setResizable(false); 56 | setIconImage(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_MEDIUM)); 57 | setLocationRelativeTo(OrganizerManager.getMainWindow()); 58 | setAlwaysOnTop(SettingsManager.isAlwaysOnTop()); 59 | 60 | addWindowListener(new WindowAdapter() { 61 | 62 | @Override 63 | public void windowOpened(WindowEvent e) 64 | { 65 | SwingUtilities.invokeLater(() -> { 66 | SwingUtilities.updateComponentTreeUI(AboutWindow.this); 67 | }); 68 | } 69 | }); 70 | } 71 | 72 | 73 | /** 74 | * 75 | */ 76 | private void initLayout() 77 | { 78 | JPanel guiPanel = new JPanel(); 79 | guiPanel.setLayout(new FlowLayout(FlowLayout.LEADING, 50, 0)); 80 | guiPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 81 | 82 | JLabel iconLabel = createIconLabel(); 83 | 84 | guiPanel.add(new AboutPanel()); 85 | guiPanel.add(iconLabel); 86 | 87 | add(guiPanel); 88 | } 89 | 90 | private JLabel createIconLabel() 91 | { 92 | JLabel iconLabel = new JLabel(new ImageIcon(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_LARGE))); 93 | iconLabel.addMouseListener(new MouseAdapter() { 94 | 95 | boolean once = false; 96 | 97 | @Override 98 | public void mouseClicked(MouseEvent e) { 99 | 100 | if(e.getClickCount() >= 3 && !once) 101 | { 102 | once = true; 103 | iconLabel.setIcon(IconsAndFontsManager.getFrankerZ()); 104 | vibrate(); 105 | } 106 | } 107 | }); 108 | 109 | return iconLabel; 110 | } 111 | 112 | 113 | private void vibrate() 114 | { 115 | try 116 | { 117 | int originalX = getLocationOnScreen().x; 118 | int originalY = getLocationOnScreen().y; 119 | for(int i = 0; i < 5; i++) { 120 | Thread.sleep(10); 121 | setLocation(originalX, originalY + 5); 122 | Thread.sleep(10); 123 | setLocation(originalX, originalY - 5); 124 | Thread.sleep(10); 125 | setLocation(originalX + 5, originalY); 126 | Thread.sleep(10); 127 | setLocation(originalX, originalY); 128 | } 129 | } 130 | catch (Exception err) 131 | { 132 | err.printStackTrace(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/OrganizerWindow.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.main; 2 | 3 | 4 | import java.awt.Dimension; 5 | import java.awt.event.MouseAdapter; 6 | import java.awt.event.MouseEvent; 7 | import java.awt.event.WindowAdapter; 8 | import java.awt.event.WindowEvent; 9 | 10 | import javax.swing.BoxLayout; 11 | import javax.swing.JFrame; 12 | import javax.swing.JPanel; 13 | import javax.swing.SwingUtilities; 14 | 15 | import com.soulsspeedruns.organizer.listeners.SettingsListener; 16 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 17 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 18 | import com.soulsspeedruns.organizer.managers.SettingsManager; 19 | 20 | 21 | /** 22 | * The Save Organizer window. 23 | *

24 | * Creates and shows the Save Organizer window. 25 | * 26 | * @author Kahmul (www.twitch.tv/kahmul78) 27 | * @date 26 Sep 2015 28 | */ 29 | public class OrganizerWindow extends JFrame implements SettingsListener 30 | { 31 | 32 | private static final int MIN_WIDTH = 640; 33 | private static final int MIN_HEIGHT = 550; 34 | 35 | private static final int MIN_WIDTH_COMPACT = 490; 36 | private static final int MIN_HEIGHT_COMPACT = 400; 37 | 38 | private static final boolean IS_RESIZABLE = true; 39 | 40 | 41 | public OrganizerWindow() 42 | { 43 | super("SoulsSpeedruns - Save Organizer"); 44 | 45 | initProperties(); 46 | initLayout(); 47 | initListeners(); 48 | 49 | setVisible(true); 50 | setExtendedState(SettingsManager.getStoredMaximizedWindowState()); 51 | } 52 | 53 | 54 | /** 55 | * Sets the properties of the window. 56 | */ 57 | private void initProperties() 58 | { 59 | setIconImage(IconsAndFontsManager.getSoulsSpeedrunsImage(IconsAndFontsManager.ICON_SIZE_MEDIUM)); 60 | setResizable(IS_RESIZABLE); 61 | setAlwaysOnTop(SettingsManager.isAlwaysOnTop()); 62 | setMinSize(SettingsManager.isCompactModeEnabled()); 63 | Dimension size = SettingsManager.getStoredWindowSize(); 64 | setSize(size); 65 | setLocationRelativeTo(null); 66 | OrganizerManager.setMainWindow(this); 67 | SettingsManager.addSettingsListener(this); 68 | } 69 | 70 | 71 | /** 72 | * Adds all components to the layout. 73 | */ 74 | private void initLayout() 75 | { 76 | JPanel guiPanel = new JPanel(); 77 | guiPanel.setLayout(new BoxLayout(guiPanel, BoxLayout.PAGE_AXIS)); 78 | 79 | guiPanel.add(new TopPanel()); 80 | guiPanel.add(new ListPanel()); 81 | guiPanel.add(new ButtonPanel()); 82 | 83 | guiPanel.addMouseListener(new MouseAdapter() 84 | { 85 | 86 | @Override 87 | public void mouseClicked(MouseEvent e) 88 | { 89 | requestFocusInWindow(); 90 | } 91 | }); 92 | 93 | add(guiPanel); 94 | } 95 | 96 | 97 | /** 98 | * Adds all the listeners to the window. 99 | */ 100 | private void initListeners() 101 | { 102 | addWindowListener(new WindowAdapter() 103 | { 104 | 105 | @Override 106 | public void windowOpened(WindowEvent e) 107 | { 108 | requestFocusInWindow(); 109 | SwingUtilities.invokeLater(() -> { 110 | SwingUtilities.updateComponentTreeUI(OrganizerWindow.this); 111 | }); 112 | } 113 | 114 | 115 | @Override 116 | public void windowClosing(WindowEvent e) 117 | { 118 | int state = getExtendedState(); 119 | if (state != JFrame.MAXIMIZED_BOTH) 120 | SettingsManager.setStoredWindowSize(new Dimension(getSize())); 121 | SettingsManager.setStoredMaximizedWindowState(state); 122 | SettingsManager.getKeyboardHook().unregisterHook(); 123 | e.getWindow().dispose(); 124 | System.exit(0); 125 | } 126 | }); 127 | } 128 | 129 | 130 | private void setMinSize(boolean isCompact) 131 | { 132 | if (isCompact) 133 | { 134 | setMinimumSize(new Dimension(MIN_WIDTH_COMPACT, MIN_HEIGHT_COMPACT)); 135 | return; 136 | } 137 | setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT)); 138 | } 139 | 140 | 141 | @Override 142 | public void settingChanged(String prefsKey) 143 | { 144 | setMinSize(SettingsManager.isCompactModeEnabled()); 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/savelist/SaveListTransferHandler.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.savelist; 2 | 3 | 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | 7 | import javax.swing.DefaultListModel; 8 | import javax.swing.DropMode; 9 | import javax.swing.JList; 10 | import javax.swing.JOptionPane; 11 | import javax.swing.TransferHandler; 12 | 13 | import com.soulsspeedruns.organizer.managers.GamesManager; 14 | 15 | 16 | /** 17 | * SaveListTransferHandler 18 | *

19 | * TransferHandler for SaveList. 20 | * 21 | * @author Kahmul (www.twitch.tv/kahmul78) 22 | * @date 27 May 2016 23 | */ 24 | public class SaveListTransferHandler extends TransferHandler 25 | { 26 | 27 | private SaveList saveList; 28 | 29 | 30 | /** 31 | * Creates a new TransferHandler for a SaveList. 32 | */ 33 | public SaveListTransferHandler(SaveList saveList) 34 | { 35 | this.saveList = saveList; 36 | } 37 | 38 | 39 | @Override 40 | public boolean canImport(TransferHandler.TransferSupport support) 41 | { 42 | if (!support.isDataFlavorSupported(SaveListEntry.ENTRY_FLAVOR)) 43 | return false; 44 | JList.DropLocation dl = (JList.DropLocation) support.getDropLocation(); 45 | 46 | int index = saveList.locationToIndex(dl.getDropPoint()); 47 | if (index == -1) 48 | return false; 49 | try 50 | { 51 | SaveListEntry entry = (SaveListEntry) support.getTransferable().getTransferData(SaveListEntry.ENTRY_FLAVOR); 52 | SaveListEntry newParentFolder = findNewParentFolderFromDropLocation(dl); 53 | if (entry.equals(newParentFolder)) 54 | return false; 55 | if (entry.isParentOf(newParentFolder)) 56 | return false; 57 | if (entry.getParent().equals(newParentFolder)) 58 | return false; 59 | support.setShowDropLocation(true); 60 | } 61 | catch (Exception e) 62 | { 63 | return false; 64 | } 65 | return true; 66 | } 67 | 68 | 69 | @Override 70 | public boolean importData(TransferHandler.TransferSupport support) 71 | { 72 | if (!canImport(support)) 73 | return false; 74 | try 75 | { 76 | SaveListEntry entry = (SaveListEntry) support.getTransferable().getTransferData(SaveListEntry.ENTRY_FLAVOR); 77 | 78 | JList.DropLocation dl = (JList.DropLocation) support.getDropLocation(); 79 | Folder newParentFolder = findNewParentFolderFromDropLocation(dl); 80 | 81 | Path newPath = Paths.get(newParentFolder.getFile().getPath()).resolve(entry.getName()); 82 | if (newPath.toFile().exists()) 83 | { 84 | if (JOptionPane.showConfirmDialog(saveList.getParent(), 85 | entry.getName() + " already exists in that directory. Do you want to overwrite?", "Confirmation", 86 | JOptionPane.YES_NO_OPTION) != 0) 87 | return false; 88 | SaveListEntry existingEntry = newParentFolder.getChildByName(entry.getName()); 89 | ((DefaultListModel) saveList.getModel()).removeElement(existingEntry); 90 | existingEntry.delete(); 91 | } 92 | 93 | entry.moveToNewParent(newParentFolder); 94 | newParentFolder.setClosed(false); 95 | saveList.refreshList(); 96 | saveList.setSelectedValue(entry, true); 97 | } 98 | catch (Exception e) 99 | { 100 | e.printStackTrace(); 101 | return false; 102 | } 103 | return true; 104 | } 105 | 106 | 107 | /** 108 | * Finds the new parent folder at the given location in the list. 109 | * 110 | * @param dl the droplocation 111 | * @return the parent folder 112 | */ 113 | private Folder findNewParentFolderFromDropLocation(JList.DropLocation dl) 114 | { 115 | int index = saveList.locationToIndex(dl.getDropPoint()); 116 | SaveListEntry newParentFolder; 117 | if (!saveList.getCellBounds(index, index).contains(dl.getDropPoint())) 118 | { 119 | newParentFolder = GamesManager.getSelectedProfile().getRoot(); 120 | saveList.setDropMode(DropMode.INSERT); 121 | } 122 | else 123 | { 124 | newParentFolder = saveList.getModel().getElementAt(index); 125 | saveList.setDropMode(DropMode.ON); 126 | if (!(saveList.getModel().getElementAt(index) instanceof Folder)) 127 | { 128 | newParentFolder = newParentFolder.getParent(); 129 | saveList.setDropMode(DropMode.INSERT); 130 | } 131 | } 132 | return (Folder) newParentFolder; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameListEntry.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import java.awt.Color; 5 | import java.awt.Graphics; 6 | import java.awt.Insets; 7 | import java.awt.datatransfer.DataFlavor; 8 | import java.awt.datatransfer.Transferable; 9 | import java.awt.datatransfer.UnsupportedFlavorException; 10 | import java.awt.event.MouseAdapter; 11 | import java.awt.event.MouseEvent; 12 | import java.io.IOException; 13 | 14 | import javax.swing.JButton; 15 | import javax.swing.UIManager; 16 | import javax.swing.border.EmptyBorder; 17 | 18 | import com.soulsspeedruns.organizer.games.Game; 19 | 20 | 21 | /** 22 | * Game List Entry 23 | *

24 | * An entry in a GameList instance. 25 | * 26 | * @author Kahmul (www.twitch.tv/kahmul78) 27 | * @date 14 Jan 2024 28 | */ 29 | public class GameListEntry extends JButton implements Transferable 30 | { 31 | 32 | public static final DataFlavor ENTRY_FLAVOR = new DataFlavor(GameListEntry.class, GameListEntry.class.getSimpleName()); 33 | 34 | private final Game game; 35 | private final GameConfigPanel configPanel; 36 | 37 | private final Color backgroundColor = new Color(UIManager.getColor("background").getRGB()); 38 | private final Color backgroundHoverColor = new Color(UIManager.getColor("backgroundHover").getRGB()); 39 | 40 | private final Color dropTargetColor = new Color(UIManager.getColor("dropForeground").getRGB()); 41 | 42 | private boolean isSelected = false; 43 | 44 | private boolean isDropTarget = false; 45 | private boolean drawDropTargetBelow = false; 46 | 47 | private GameList list; 48 | 49 | 50 | public GameListEntry(Game game, GameList list) 51 | { 52 | String caption = game.getCaption(); 53 | if (caption.length() > 22) 54 | { 55 | caption = caption.substring(0, 21); 56 | caption += "..."; 57 | } 58 | 59 | setText(caption); 60 | 61 | this.game = game; 62 | this.list = list; 63 | configPanel = new GameConfigPanel(game); 64 | 65 | setFocusPainted(false); 66 | setBackground(backgroundColor); 67 | setMargin(new Insets(7, 12, 7, 150)); 68 | setBorder(new EmptyBorder(0, 0, 0, 0)); 69 | 70 | addMouseListener(new MouseAdapter() 71 | { 72 | @Override 73 | public void mousePressed(MouseEvent e) 74 | { 75 | e.consume(); 76 | list.setSelectedEntry(GameListEntry.this); 77 | } 78 | }); 79 | 80 | new GameListEntryDragListener(this, list); 81 | } 82 | 83 | 84 | public boolean isSelected() 85 | { 86 | return isSelected; 87 | } 88 | 89 | 90 | public void setSelected(boolean flag) 91 | { 92 | isSelected = flag; 93 | setBackground(flag ? backgroundHoverColor : backgroundColor); 94 | } 95 | 96 | 97 | /** 98 | * Sets whether this entry is highlighted as a potential drop location in the list. 99 | * 100 | * @param isDropTarget whether to highlight this entry as a drop location 101 | * @param drawDropTargetBelow whether to draw the drop line below or above the entry 102 | */ 103 | public void setIsDropTarget(boolean isDropTarget, boolean drawDropTargetBelow) 104 | { 105 | if(this.isDropTarget == isDropTarget && this.drawDropTargetBelow == drawDropTargetBelow) 106 | return; 107 | this.isDropTarget = isDropTarget; 108 | this.drawDropTargetBelow = drawDropTargetBelow; 109 | repaint(); 110 | } 111 | 112 | 113 | @Override 114 | public void paint(Graphics g) 115 | { 116 | super.paint(g); 117 | 118 | if(isDropTarget) 119 | { 120 | g.setColor(dropTargetColor); 121 | if(drawDropTargetBelow) 122 | { 123 | g.fillRect(0, getHeight() - 2, getWidth() - 1, 2); 124 | return; 125 | } 126 | g.fillRect(0, 0, getWidth() - 1, 2); 127 | } 128 | } 129 | 130 | 131 | public Game getGame() 132 | { 133 | return game; 134 | } 135 | 136 | 137 | public GameConfigPanel getConfigPanel() 138 | { 139 | return configPanel; 140 | } 141 | 142 | 143 | @Override 144 | public DataFlavor[] getTransferDataFlavors() 145 | { 146 | return new DataFlavor[] { ENTRY_FLAVOR }; 147 | } 148 | 149 | 150 | @Override 151 | public boolean isDataFlavorSupported(DataFlavor flavor) 152 | { 153 | return flavor.equals(ENTRY_FLAVOR); 154 | } 155 | 156 | 157 | @Override 158 | public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException 159 | { 160 | if (isDataFlavorSupported(flavor)) 161 | return list.getIndexOfEntry(this); 162 | throw new UnsupportedFlavorException(flavor); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/hotkeys/GlobalHotkey.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.hotkeys; 2 | 3 | 4 | import javax.swing.JOptionPane; 5 | 6 | import com.soulsspeedruns.organizer.managers.GamesManager; 7 | import com.soulsspeedruns.organizer.managers.SavesManager; 8 | import com.soulsspeedruns.organizer.managers.SettingsManager; 9 | import com.soulsspeedruns.organizer.savelist.Save; 10 | 11 | 12 | /** 13 | * Global Hotkey Enum. 14 | *

15 | * Enum representing the different global hotkeys and their actions. 16 | * 17 | * @author Kahmul (www.twitch.tv/kahmul78) 18 | * @date 4 Jun 2016 19 | */ 20 | public enum GlobalHotkey 21 | { 22 | 23 | LOAD_SAVE("Load Savestate:", "None") 24 | { 25 | 26 | @Override 27 | public void action() 28 | { 29 | if (SavesManager.getSelectedEntry() instanceof Save) 30 | SavesManager.loadSave((Save) SavesManager.getSelectedEntry()); 31 | } 32 | 33 | 34 | @Override 35 | public String getPrefsKey() 36 | { 37 | return SettingsManager.PREFS_KEY_GLOBAL_HOTKEY_LOAD; 38 | } 39 | }, 40 | IMPORT_SAVE("Import Savestate:", "None") 41 | { 42 | 43 | @Override 44 | public void action() 45 | { 46 | if (GamesManager.isAProfileSelected()) 47 | { 48 | SavesManager.importSavefile(null); 49 | return; 50 | } 51 | JOptionPane.showMessageDialog(null, 52 | "Create a profile before trying to import a savefile! You can do this in the profile configuration settings.", 53 | "Warning", JOptionPane.WARNING_MESSAGE); 54 | } 55 | 56 | 57 | @Override 58 | public String getPrefsKey() 59 | { 60 | return SettingsManager.PREFS_KEY_GLOBAL_HOTKEY_IMPORT_SAVE; 61 | } 62 | }, 63 | READ_ONLY_TOGGLE("Switch Gamefile To Read-Only:", "None") 64 | { 65 | 66 | @Override 67 | public void action() 68 | { 69 | SavesManager.switchCurrentGameFileWritableState(); 70 | } 71 | 72 | 73 | @Override 74 | public String getPrefsKey() 75 | { 76 | return SettingsManager.PREFS_KEY_GLOBAL_HOTKEY_READ_ONLY; 77 | } 78 | }, 79 | PREV_SAVE_TOGGLE("Highlight Previous Savestate:", "None") 80 | { 81 | 82 | @Override 83 | public void action() 84 | { 85 | SavesManager.fireNavigatedToPreviousEvent(); 86 | } 87 | 88 | 89 | @Override 90 | public String getPrefsKey() 91 | { 92 | return SettingsManager.PREFS_KEY_GLOBAL_HOTKEY_PREV_SAVE; 93 | } 94 | }, 95 | NEXT_SAVE_TOGGLE("Highlight Next Savestate:", "None") 96 | { 97 | 98 | @Override 99 | public void action() 100 | { 101 | SavesManager.fireNavigatedToNextEvent(); 102 | } 103 | 104 | 105 | @Override 106 | public String getPrefsKey() 107 | { 108 | return SettingsManager.PREFS_KEY_GLOBAL_HOTKEY_NEXT_SAVE; 109 | } 110 | }, 111 | GLOBAL_HOTKEY_TOGGLE("Toggle Global Hotkeys:", "None") 112 | { 113 | 114 | @Override 115 | public void action() 116 | { 117 | SettingsManager.setGlobalHotkeysEnabled(!SettingsManager.areGlobalHotkeysEnabled()); 118 | } 119 | 120 | 121 | @Override 122 | public String getPrefsKey() 123 | { 124 | return SettingsManager.PREFS_KEY_GLOBAL_HOTKEY_TOGGLE; 125 | } 126 | }; 127 | 128 | private String caption; 129 | private String keyCode; 130 | 131 | 132 | /** 133 | * Creates a new GlobalHotkey constant. 134 | * 135 | * @param caption the name of the hotkey which is displayed in the settings window 136 | * @param keyCode the associated key code on which the hotkey action will be executed 137 | */ 138 | private GlobalHotkey(String caption, String keyCode) 139 | { 140 | setCaption(caption); 141 | this.keyCode = keyCode; 142 | } 143 | 144 | 145 | /** 146 | * The action to perform on pressing the hotkey. 147 | */ 148 | public abstract void action(); 149 | 150 | 151 | public abstract String getPrefsKey(); 152 | 153 | 154 | /** 155 | * @return the caption 156 | */ 157 | public String getCaption() 158 | { 159 | return caption; 160 | } 161 | 162 | 163 | /** 164 | * @param caption the caption to set 165 | */ 166 | public void setCaption(String caption) 167 | { 168 | this.caption = caption; 169 | } 170 | 171 | 172 | /** 173 | * @return the keyCode 174 | */ 175 | public String getKeyCode() 176 | { 177 | String storedCode = SettingsManager.getStoredHotkeyCode(this); 178 | return "".equals(storedCode) ? keyCode : storedCode; 179 | 180 | } 181 | 182 | 183 | /** 184 | * @param keyCode the keyCode to set 185 | */ 186 | public void setKeyCode(String keyCode) 187 | { 188 | this.keyCode = keyCode; 189 | SettingsManager.setStoredHotkeyCode(this, keyCode); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/main/TopPanel.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package com.soulsspeedruns.organizer.main; 5 | 6 | 7 | import javax.swing.GroupLayout; 8 | import javax.swing.GroupLayout.Alignment; 9 | import javax.swing.JButton; 10 | import javax.swing.JLabel; 11 | import javax.swing.JPanel; 12 | import javax.swing.SwingConstants; 13 | 14 | import com.soulsspeedruns.organizer.components.SearchBar; 15 | import com.soulsspeedruns.organizer.games.config.GamesConfigurationWindow; 16 | import com.soulsspeedruns.organizer.main.config.GamesComboBox; 17 | import com.soulsspeedruns.organizer.main.config.ProfilesComboBox; 18 | import com.soulsspeedruns.organizer.main.config.SortingComboBox; 19 | import com.soulsspeedruns.organizer.managers.GamesManager; 20 | 21 | 22 | /** 23 | * Top segment of the main window. 24 | *

25 | * Contains the games and profile comboboxes, as well as the Edit Games button and search bar. 26 | * 27 | * @author Kahmul (www.twitch.tv/kahmul78) 28 | * @date 21 Jan 2024 29 | */ 30 | public class TopPanel extends JPanel 31 | { 32 | 33 | /** 34 | * Creates the top panel for the main window. 35 | */ 36 | protected TopPanel() 37 | { 38 | GroupLayout layout = new GroupLayout(this); 39 | layout.setAutoCreateGaps(true); 40 | layout.setAutoCreateContainerGaps(true); 41 | 42 | JLabel gamesLabel = new JLabel("Game:"); 43 | JLabel profilesLabel = new JLabel("Profile:"); 44 | 45 | SearchBar searchBar = new SearchBar(); 46 | 47 | ProfilesComboBox profilesComboBox = new ProfilesComboBox(GamesManager.getSelectedGame()); 48 | GamesComboBox gamesComboBox = new GamesComboBox(profilesComboBox); 49 | 50 | JPanel sortingPanel = createSortingPanel(); 51 | JPanel profilesPanel = createProfilesPanel(profilesComboBox); 52 | 53 | layout.linkSize(SwingConstants.HORIZONTAL, gamesComboBox, sortingPanel); 54 | 55 | // Horizontal 56 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 57 | 58 | hGroup.addGroup(layout.createParallelGroup().addComponent(gamesLabel) 59 | .addComponent(gamesComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 60 | .addComponent(sortingPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)); 61 | hGroup.addGroup( 62 | layout.createParallelGroup().addComponent(profilesPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE) 63 | .addComponent(profilesLabel).addComponent(searchBar)); 64 | 65 | layout.setHorizontalGroup(hGroup); 66 | 67 | // Vertical 68 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 69 | 70 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(gamesLabel).addComponent(profilesLabel)); 71 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE) 72 | .addComponent(gamesComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 73 | .addComponent(profilesPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)); 74 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE) 75 | .addComponent(sortingPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 76 | .addComponent(searchBar)); 77 | vGroup.addGap(10); 78 | 79 | layout.setVerticalGroup(vGroup); 80 | 81 | setLayout(layout); 82 | } 83 | 84 | 85 | /** 86 | * Creates the panel containing the sorting combobox and label. 87 | * 88 | * @return the sorting panel 89 | */ 90 | private JPanel createSortingPanel() 91 | { 92 | JLabel sortByLabel = new JLabel("Sort by:"); 93 | SortingComboBox sortingComboBox = new SortingComboBox(); 94 | 95 | JPanel sortingPanel = new JPanel(); 96 | GroupLayout panelLayout = new GroupLayout(sortingPanel); 97 | 98 | panelLayout.setHorizontalGroup(panelLayout.createSequentialGroup().addComponent(sortByLabel).addGap(5).addComponent(sortingComboBox)); 99 | panelLayout.setVerticalGroup(panelLayout.createParallelGroup(Alignment.CENTER).addComponent(sortByLabel).addComponent(sortingComboBox)); 100 | 101 | sortingPanel.setLayout(panelLayout); 102 | 103 | return sortingPanel; 104 | } 105 | 106 | 107 | /** 108 | * Creates the panel containing the profiles combobox and Edit Games button. 109 | * 110 | * @param profilesComboBox the profiles combobox of this top panel 111 | * @return the profiles panel 112 | */ 113 | private JPanel createProfilesPanel(ProfilesComboBox profilesComboBox) 114 | { 115 | JButton editButton = createEditProfilesButton(); 116 | 117 | JPanel profilesPanel = new JPanel(); 118 | GroupLayout panelLayout = new GroupLayout(profilesPanel); 119 | 120 | panelLayout.setHorizontalGroup(panelLayout.createSequentialGroup() 121 | .addComponent(profilesComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE).addGap(5) 122 | .addComponent(editButton)); 123 | panelLayout.setVerticalGroup(panelLayout.createParallelGroup() 124 | .addComponent(profilesComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 125 | .addComponent(editButton)); 126 | 127 | profilesPanel.setLayout(panelLayout); 128 | 129 | return profilesPanel; 130 | } 131 | 132 | 133 | /** 134 | * Creates the 'Edit profiles' button. 135 | * 136 | * @return the edit profiles button 137 | */ 138 | private JButton createEditProfilesButton() 139 | { 140 | JButton editButton = new JButton("Edit Games"); 141 | editButton.addActionListener(event -> { 142 | new GamesConfigurationWindow(); 143 | }); 144 | return editButton; 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/savelist/Folder.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.savelist; 2 | 3 | 4 | import java.awt.Color; 5 | import java.awt.Font; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.Files; 9 | import java.util.Collections; 10 | import java.util.LinkedList; 11 | import java.util.List; 12 | 13 | import javax.swing.BorderFactory; 14 | import javax.swing.Icon; 15 | import javax.swing.JLabel; 16 | import javax.swing.JList; 17 | import javax.swing.JOptionPane; 18 | import javax.swing.UIManager; 19 | 20 | import com.soulsspeedruns.organizer.managers.SavesManager; 21 | 22 | import jiconfont.icons.FontAwesome; 23 | import jiconfont.swing.IconFontSwing; 24 | 25 | 26 | /** 27 | * Folder class. 28 | *

29 | * Represents the folders in the SaveList. 30 | * 31 | * @author Kahmul (www.twitch.tv/kahmul78) 32 | * @date 7 Jul 2017 33 | */ 34 | public class Folder extends SaveListEntry 35 | { 36 | 37 | private boolean isClosed = true; 38 | 39 | private static final Color ICON_COLOR = new Color(251, 208, 108); 40 | private static final Color ICON_COLOR_ERROR = Color.RED; 41 | private static final int ICON_SIZE = 15; 42 | 43 | private static final Icon ICON = IconFontSwing.buildIcon(FontAwesome.FOLDER, ICON_SIZE, ICON_COLOR); 44 | private static final Icon ICON_OPEN = IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN, ICON_SIZE, ICON_COLOR); 45 | 46 | private static final Icon ICON_ERROR = IconFontSwing.buildIcon(FontAwesome.FOLDER, ICON_SIZE, ICON_COLOR_ERROR); 47 | private static final Icon ICON_OPEN_ERROR = IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN, ICON_SIZE, ICON_COLOR_ERROR); 48 | 49 | 50 | /** 51 | * Creates a new Folder instance. Children in the underlying filesystem are automatically added. 52 | * 53 | * @param parent the parent folder 54 | * @param file the associated file 55 | */ 56 | public Folder(Folder parent, File file) 57 | { 58 | super(parent, file); 59 | File[] files = file.listFiles(); 60 | for (File currentFile : files) 61 | { 62 | if (currentFile.isDirectory()) 63 | { 64 | addChild(new Folder(this, currentFile)); 65 | continue; 66 | } 67 | addChild(new Save(this, currentFile)); 68 | } 69 | Collections.sort(getChildren()); 70 | } 71 | 72 | /** 73 | * @return whether the folder is closed or not 74 | */ 75 | public boolean isClosed() 76 | { 77 | return isClosed; 78 | } 79 | 80 | 81 | /** 82 | * @param isClosed set the folder state closed or opened 83 | */ 84 | public void setClosed(boolean isClosed) 85 | { 86 | this.isClosed = isClosed; 87 | } 88 | 89 | 90 | @Override 91 | public void removeChild(SaveListEntry entry) 92 | { 93 | super.removeChild(entry); 94 | if (getChildren().size() == 0) 95 | setClosed(true); 96 | } 97 | 98 | 99 | @Override 100 | public boolean rename(String newName) 101 | { 102 | File newFile = new File(getParent().getFile() + File.separator + newName); 103 | try 104 | { 105 | // if the same name is given, then only the file variable is supposed to be updated for a new parent 106 | if (!getFile().getName().equals((newName))) 107 | Files.move(getFile().toPath(), newFile.toPath()); 108 | } 109 | catch (IOException e) 110 | { 111 | JOptionPane.showMessageDialog(null, 112 | "Renaming the entries was not successful. They are possibly being accessed by another program.", "Warning", 113 | JOptionPane.WARNING_MESSAGE); 114 | return false; 115 | } 116 | setFile(newFile); 117 | for (SaveListEntry entry : getChildren()) 118 | { 119 | // call rename on all children with the same name to update the path with the new parent 120 | if (!entry.rename(entry.getName())) 121 | return false; 122 | } 123 | return true; 124 | } 125 | 126 | 127 | @Override 128 | public boolean canBeRenamed() 129 | { 130 | if (getFile().canWrite()) 131 | { 132 | for (SaveListEntry entry : getChildren()) 133 | { 134 | if (!entry.canBeRenamed()) 135 | return false; 136 | } 137 | return true; 138 | } 139 | return false; 140 | } 141 | 142 | 143 | @Override 144 | public void delete() 145 | { 146 | List children = new LinkedList<>(getChildren()); 147 | for (SaveListEntry child : children) 148 | { 149 | child.delete(); 150 | } 151 | clearChildren(); 152 | getParent().removeChild(this); 153 | getFile().delete(); 154 | } 155 | 156 | 157 | @Override 158 | public void render(JList list, int index, JLabel label) 159 | { 160 | label.setText(getFile().getName()); 161 | label.setFont(label.getFont().deriveFont(Font.BOLD)); 162 | label.setBorder(BorderFactory.createEmptyBorder(1, 3 + getIndent(), 0, 1)); 163 | 164 | JList.DropLocation dropLocation = list.getDropLocation(); 165 | if (dropLocation != null && dropLocation.getIndex() == index) 166 | { 167 | label.setBackground(new Color(UIManager.getColor("List.backgroundSelected").getRGB())); 168 | label.setForeground(new Color(UIManager.getColor("List.foregroundSelected").getRGB())); 169 | } 170 | 171 | if(isMarkedForCut()) 172 | label.setForeground(Color.GRAY); 173 | 174 | if (!getFile().exists()) 175 | { 176 | label.setForeground(ICON_COLOR_ERROR); 177 | label.setToolTipText("Directory does not exist any longer!"); 178 | 179 | label.setIcon(isClosed() ? ICON_ERROR : ICON_OPEN_ERROR); 180 | return; 181 | } 182 | label.setIcon(isClosed() ? ICON : ICON_OPEN); 183 | 184 | } 185 | 186 | 187 | @Override 188 | public int compareTo(SaveListEntry entry) 189 | { 190 | if (entry instanceof Save) 191 | return -1; 192 | return SavesManager.getSelectedSortingCategory().compare(this, entry); 193 | } 194 | 195 | } 196 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/theme/soulsspeedruns_defaults.properties: -------------------------------------------------------------------------------- 1 | Theme.dark = true 2 | Theme.highContrast = false 3 | 4 | 5 | %soulsSpeedrunsBackground = #202937 6 | %soulsSpeedrunsComponentBackground = #2b384a 7 | %soulsspeedrunsListSelectionForeground = #fdba74 8 | %soulsspeedrunsListSelectionBackground = #3b3e45 9 | %soulsspeedrunsLinkColor = #fdba74 10 | 11 | ####Background#### 12 | %background = #202937 13 | %backgroundAlternative = #2C313A 14 | %backgroundColorful = #414855 15 | %backgroundColorfulInactive = #fdba74 16 | %backgroundContainer = #2b384a 17 | 18 | %backgroundHeader = #202937 19 | %backgroundToolTip = #3D424B 20 | %backgroundToolTipInactive = #3D424B 21 | 22 | %backgroundHover = #3b3e45 23 | %backgroundSelected = #404040 24 | %backgroundHoverSecondary = #323844 25 | %backgroundSelectedSecondary = #3D424B 26 | %backgroundHoverColorful = #3D424B 27 | %backgroundSelectedColorful = #323844 28 | 29 | %dropBackground = #3b3e45 30 | %dropForeground = #fdba74 31 | 32 | ####Border#### 33 | %borderSecondary = #46494F 34 | %border = #46494F 35 | %borderTertiary = #46494F 36 | %borderFocus = #fb923c 37 | %gridLine = #32363C 38 | 39 | ####Highlight#### 40 | %hoverHighlight = #3b3e45 41 | %clickHighlight = #3D424B 42 | 43 | %hoverHighlightOutline = #3D424B 44 | %clickHighlightOutline = #333841 45 | 46 | %hoverHighlightColorful = #3D424B 47 | %clickHighlightColorful = #333841 48 | 49 | %hoverHighlightDefault = #7AA3F5 50 | %clickHighlightDefault = #6897F3 51 | 52 | %hoverHighlightSecondary = #323844 53 | %clickHighlightSecondary = #323844 54 | 55 | %highlightFill = #2C313A 56 | %highlightFillFocus = #3b3e45 57 | %highlightFillFocusSecondary = #fb923c 58 | %highlightFillMono = #ef9d5a 59 | 60 | ####Widgets#### 61 | %widgetBorder = #464C55 62 | %widgetBorderInactive = #2D3137 63 | %widgetBorderDefault = #fb923c 64 | %widgetFill = #2b384a 65 | %widgetFillSelected = #333841 66 | %widgetFillInactive = #202937 67 | %widgetFillDefault = #fb923c 68 | 69 | ####Controls#### 70 | %controlBorder = #414855 71 | %controlBorderDisabled = #2C313A 72 | %controlBorderSelected = #414855 73 | %controlBorderFocus = #fb923c 74 | %controlBorderFocusSelected = #fb923c 75 | %controlBorderSecondary = #282B2F 76 | %controlFill = #ABB2BF 77 | %controlFillFocus = #ABB2BF 78 | %controlFillSecondary = #fb923c 79 | %controlTrack = #434852 80 | %controlFillDisabled = #5C6370 81 | %controlFillHighlight = #fb923c 82 | %controlFillHighlightDisabled = #313469 83 | %controlBackground = #1D1D26 84 | %controlFadeStart = #fb923c 85 | %controlFadeEnd = #313469 86 | %controlFadeStartSecondary = #676A71 87 | %controlFadeEndSecondary = #868A91 88 | %controlErrorFadeStart = #bd3c5f 89 | %controlErrorFadeEnd = #472c33 90 | %controlPassedFadeStart = #239E62 91 | %controlPassedFadeEnd = #2b4242 92 | 93 | ####Text#### 94 | %caret = #abb2bf 95 | %textForeground = #f2f2f2 96 | %textForegroundDefault = #212121 97 | %textForegroundHighlight = #abb2bf 98 | %textForegroundInactive = #777777 99 | %textForegroundSecondary = #f2f2f2 100 | %acceleratorForeground = #E6E6E6 101 | %textContrastForeground = #FFFFFF 102 | 103 | ## changed 104 | %textSelectionForeground = #fdba74 105 | %textSelectionForegroundInactive = #d7dae0 106 | %textSelectionForegroundDisabled = #d7dae0 107 | %textSelectionBackground = #4d78cc 108 | %textSelectionBackgroundSecondary = #2C313C 109 | %textBackground = #282c34 110 | %textBackgroundInactive = #202937 111 | %textBackgroundSecondary = #282C34 112 | %textBackgroundSecondaryInactive = #282C34 113 | 114 | %textCompSelectionForeground = #d7dae0 115 | %textCompSelectionBackground = #4d78cc 116 | 117 | %hyperlink = #fdba74 118 | %hyperlinkAccent = #6494ED 119 | 120 | ####Misc#### 121 | %shadow = #202937 122 | 123 | %glowOpacity = 70 124 | %dropOpacity = 80 125 | %shadowOpacityLight = 10 126 | %shadowOpacityStrong = 20 127 | 128 | ## changed 129 | %glowFocus = #fdba74 130 | %glowFocusInactive = #fdba74 131 | %glowFocusLine = #fdba74 132 | %glowFocusLineInactive = #fdba74 133 | ## changed 134 | 135 | %glowError = #522530 136 | %glowErrorLine = #692746 137 | 138 | %glowFocusError = #802d43 139 | %glowFocusErrorLine = #692746 140 | 141 | %glowWarning = #8c812b 142 | %glowWarningLine = #5f4422 143 | 144 | #Arc 145 | %arc = 5 146 | %arcFocus = 5 147 | %arcSecondary = 3 148 | %arcSecondaryFocus = 3 149 | 150 | %borderThickness = 2 151 | %shadowHeight = 2 152 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/hotkeys/GlobalKeyboardHook.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.hotkeys; 2 | 3 | 4 | import java.awt.Toolkit; 5 | import java.awt.event.KeyEvent; 6 | import java.util.logging.Level; 7 | import java.util.logging.Logger; 8 | 9 | import org.jnativehook.GlobalScreen; 10 | import org.jnativehook.NativeHookException; 11 | import org.jnativehook.keyboard.NativeKeyEvent; 12 | import org.jnativehook.keyboard.NativeKeyListener; 13 | 14 | 15 | /** 16 | * GlobalKeyboardHook. 17 | *

18 | * Registers global hotkeys. 19 | * 20 | * @author Kahmul (www.twitch.tv/kahmul78) 21 | * @date 20 May 2016 22 | */ 23 | public class GlobalKeyboardHook implements NativeKeyListener 24 | { 25 | 26 | private boolean hotkeysEnabled = false; 27 | 28 | // if a modifier key is released, this is used to decide whether the modifier was tied to a previous key combo or not 29 | private boolean keyComboWasExecuted = false; 30 | 31 | 32 | /** 33 | * Registers a global hotkey hook. 34 | * 35 | * @throws NativeHookException 36 | */ 37 | public GlobalKeyboardHook() throws NativeHookException 38 | { 39 | Logger logger = Logger.getLogger(GlobalScreen.class.getPackage().getName()); 40 | logger.setLevel(Level.OFF); 41 | GlobalScreen.registerNativeHook(); 42 | GlobalScreen.addNativeKeyListener(this); 43 | } 44 | 45 | 46 | /** 47 | * Registers the global hotkey hook. 48 | */ 49 | public void registerHook() 50 | { 51 | try 52 | { 53 | GlobalScreen.registerNativeHook(); 54 | } 55 | catch (NativeHookException e) 56 | { 57 | e.printStackTrace(); 58 | } 59 | } 60 | 61 | 62 | /** 63 | * Unregisters the global hotkey hook. 64 | */ 65 | public void unregisterHook() 66 | { 67 | try 68 | { 69 | GlobalScreen.unregisterNativeHook(); 70 | } 71 | catch (NativeHookException e) 72 | { 73 | e.printStackTrace(); 74 | } 75 | } 76 | 77 | 78 | /** 79 | * Toggles global hotkeys. 80 | * 81 | * @param flag True to enable hotkeys, false to disable them 82 | */ 83 | public void setHotkeysEnabled(boolean flag) 84 | { 85 | hotkeysEnabled = flag; 86 | } 87 | 88 | 89 | /** 90 | * Returns whether global hotkeys are enabled. 91 | * 92 | * @return whether global hotkeys are enabled 93 | */ 94 | public boolean areHotkeysEnabled() 95 | { 96 | return hotkeysEnabled; 97 | } 98 | 99 | 100 | @Override 101 | public void nativeKeyTyped(NativeKeyEvent e) 102 | { 103 | } 104 | 105 | 106 | @Override 107 | public void nativeKeyPressed(NativeKeyEvent e) 108 | { 109 | keyComboWasExecuted = false; 110 | int keyCode = e.getKeyCode(); 111 | if (keyCode == NativeKeyEvent.VC_SHIFT_L || keyCode == NativeKeyEvent.VC_SHIFT_R || keyCode == NativeKeyEvent.VC_CONTROL_L 112 | || keyCode == NativeKeyEvent.VC_CONTROL_R || keyCode == NativeKeyEvent.VC_ALT_L || keyCode == NativeKeyEvent.VC_ALT_R) 113 | return; 114 | executeKeyEvent(e); 115 | } 116 | 117 | 118 | @Override 119 | public void nativeKeyReleased(NativeKeyEvent e) 120 | { 121 | int keyCode = e.getKeyCode(); 122 | if (keyCode == NativeKeyEvent.VC_SHIFT_L || keyCode == NativeKeyEvent.VC_SHIFT_R || keyCode == NativeKeyEvent.VC_CONTROL_L 123 | || keyCode == NativeKeyEvent.VC_CONTROL_R || keyCode == NativeKeyEvent.VC_ALT_L || keyCode == NativeKeyEvent.VC_ALT_R) 124 | { 125 | // if only modifiers have been held so far and one is released, then execute the action 126 | if (!keyComboWasExecuted) 127 | executeKeyEvent(e); 128 | } 129 | } 130 | 131 | 132 | /** 133 | * Executes the action of the hotkey with the same key code as the given KeyEvent. 134 | * 135 | * @param e the KeyEvent 136 | */ 137 | private void executeKeyEvent(NativeKeyEvent e) 138 | { 139 | String keyText = getKeyText(e); 140 | for (GlobalHotkey hotkey : GlobalHotkey.values()) 141 | { 142 | if (!hotkeysEnabled && hotkey != GlobalHotkey.GLOBAL_HOTKEY_TOGGLE) 143 | continue; 144 | if (keyText.equals(hotkey.getKeyCode())) 145 | { 146 | hotkey.action(); 147 | keyComboWasExecuted = true; 148 | break; 149 | } 150 | } 151 | } 152 | 153 | 154 | /** 155 | * Gets the key combination text from the KeyEvent. 156 | * 157 | * @param e the KeyEvent 158 | * @return the combined text of all pressed keys 159 | */ 160 | private String getKeyText(NativeKeyEvent e) 161 | { 162 | String modifiers = NativeKeyEvent.getModifiersText(e.getModifiers()); 163 | if (modifiers.length() > 0) 164 | { 165 | modifiers = modifiers.replaceAll("\\+", " \\+ "); 166 | // replace "Shift + Ctrl/Alt" with "Ctrl/Alt + Shift" for compatibility between global hotkeys and base Java hotkeys 167 | modifiers = modifiers.replaceAll(Toolkit.getProperty("AWT.shift", "Shift") + " \\+ " + Toolkit.getProperty("AWT.control", "Ctrl"), 168 | Toolkit.getProperty("AWT.control", "Ctrl") + " \\+ " + Toolkit.getProperty("AWT.shift", "Shift")); 169 | modifiers = modifiers.replaceAll(Toolkit.getProperty("AWT.shift", "Shift") + " \\+ " + Toolkit.getProperty("AWT.alt", "Alt"), 170 | Toolkit.getProperty("AWT.alt", "alt") + " \\+ " + Toolkit.getProperty("AWT.shift", "Shift")); 171 | modifiers += " + "; 172 | } 173 | 174 | String keyText = ""; 175 | 176 | // NativeKeyEvent has different keytexts than Java for modifiers, so need to standardize them 177 | switch (e.getKeyCode()) 178 | { 179 | case NativeKeyEvent.VC_CONTROL_L: 180 | case NativeKeyEvent.VC_CONTROL_R: 181 | keyText = KeyEvent.getKeyText(KeyEvent.VK_CONTROL); 182 | break; 183 | case NativeKeyEvent.VC_SHIFT_L: 184 | case NativeKeyEvent.VC_SHIFT_R: 185 | keyText = KeyEvent.getKeyText(KeyEvent.VK_SHIFT); 186 | break; 187 | case NativeKeyEvent.VC_ALT_L: 188 | case NativeKeyEvent.VC_ALT_R: 189 | keyText = KeyEvent.getKeyText(KeyEvent.VK_ALT); 190 | break; 191 | default: 192 | keyText = NativeKeyEvent.getKeyText(e.getKeyCode()); 193 | break; 194 | } 195 | 196 | return modifiers + keyText; 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GamesConfigPane.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import java.awt.Color; 5 | import java.awt.Insets; 6 | 7 | import javax.swing.Box; 8 | import javax.swing.BoxLayout; 9 | import javax.swing.GroupLayout; 10 | import javax.swing.JButton; 11 | import javax.swing.JLabel; 12 | import javax.swing.JOptionPane; 13 | import javax.swing.JPanel; 14 | import javax.swing.SwingUtilities; 15 | import javax.swing.UIManager; 16 | import javax.swing.border.EmptyBorder; 17 | import javax.swing.border.MatteBorder; 18 | 19 | import jiconfont.icons.FontAwesome; 20 | import jiconfont.swing.IconFontSwing; 21 | 22 | 23 | /** 24 | * Games Config Pane 25 | *

26 | * Component pane offering all available games in a scrollable list, together with a configuration panel and buttons to add/delete games. 27 | * 28 | * @author Kahmul (www.twitch.tv/kahmul78) 29 | * @date 11 Jan 2024 30 | */ 31 | public class GamesConfigPane extends JPanel implements GameListListener 32 | { 33 | 34 | private JButton editButton; 35 | private JButton deleteButton; 36 | private JLabel settingsLabel; 37 | 38 | private GroupLayout gameSelectionLayout; 39 | private GameList gameList; 40 | 41 | 42 | public GamesConfigPane() 43 | { 44 | super(); 45 | 46 | initLayout(); 47 | } 48 | 49 | 50 | private void initLayout() 51 | { 52 | setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); 53 | 54 | add(createButtonsPanel()); 55 | add(createGameSelectionPanel()); 56 | } 57 | 58 | 59 | private JPanel createButtonsPanel() 60 | { 61 | JPanel buttonPanel = new JPanel(); 62 | buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS)); 63 | buttonPanel.setBorder(new EmptyBorder(0, 0, 0, 0)); 64 | 65 | Color buttonColor = new Color(UIManager.getColor("background").getRGB()); 66 | 67 | JButton addButton = createButton(FontAwesome.PLUS, buttonColor); 68 | addButton.addActionListener(e -> { 69 | new GameCreationWindow(gameList, null); 70 | }); 71 | 72 | editButton = createButton(FontAwesome.PENCIL, buttonColor); 73 | editButton.addActionListener(e -> { 74 | new GameCreationWindow(gameList, gameList.getSelectedEntry().getGame()); 75 | }); 76 | 77 | deleteButton = createButton(FontAwesome.TRASH, buttonColor); 78 | deleteButton.addActionListener((e) -> { 79 | int confirm = JOptionPane.showConfirmDialog(SwingUtilities.windowForComponent(this), 80 | "Do you really want to delete your custom game '" + gameList.getSelectedEntry().getGame().getCaption() + "'?", 81 | "Delete Custom Game", JOptionPane.YES_NO_OPTION); 82 | if (confirm == 0) 83 | gameList.deleteSelectedEntry(); 84 | }); 85 | 86 | settingsLabel = new JLabel("Game Settings"); 87 | 88 | buttonPanel.add(addButton); 89 | buttonPanel.add(editButton); 90 | buttonPanel.add(deleteButton); 91 | 92 | buttonPanel.add(Box.createHorizontalGlue()); 93 | buttonPanel.add(settingsLabel); 94 | buttonPanel.add(Box.createHorizontalGlue()); 95 | 96 | buttonPanel.setBorder(new MatteBorder(0, 0, 1, 0, UIManager.getColor("borderSecondary"))); 97 | 98 | return buttonPanel; 99 | } 100 | 101 | 102 | private JButton createButton(FontAwesome icon, Color buttonColor) 103 | { 104 | JButton button = new JButton(IconFontSwing.buildIcon(icon, 16, UIManager.getColor("gameConfigButtonColors"))); 105 | button.setDisabledIcon(IconFontSwing.buildIcon(icon, 16, UIManager.getColor("disabledIconColor"))); 106 | button.setBorderPainted(false); 107 | button.setBackground(buttonColor); 108 | button.setMargin(new Insets(6, 18, 6, 18)); 109 | button.setBorder(new EmptyBorder(0, 0, 0, 0)); 110 | 111 | return button; 112 | } 113 | 114 | 115 | private JPanel createGameSelectionPanel() 116 | { 117 | JPanel gameSelectionPanel = new JPanel(); 118 | 119 | gameSelectionLayout = new GroupLayout(gameSelectionPanel); 120 | 121 | gameList = new GameList(); 122 | gameList.addListener(this); 123 | updateButtonState(gameList.getSelectedEntry()); 124 | 125 | GameConfigPanel configPanel = gameList.getSelectedEntry().getConfigPanel(); 126 | 127 | GroupLayout.SequentialGroup hGroup = gameSelectionLayout.createSequentialGroup() 128 | .addComponent(gameList, GroupLayout.PREFERRED_SIZE, 151, GroupLayout.PREFERRED_SIZE) 129 | .addComponent(configPanel, GroupLayout.PREFERRED_SIZE, 435, GroupLayout.PREFERRED_SIZE); 130 | 131 | GroupLayout.ParallelGroup vGroup = gameSelectionLayout.createParallelGroup() 132 | .addComponent(gameList, GroupLayout.PREFERRED_SIZE, configPanel.getPreferredSize().height, GroupLayout.PREFERRED_SIZE) 133 | .addComponent(configPanel); 134 | 135 | gameSelectionLayout.setHorizontalGroup(hGroup); 136 | gameSelectionLayout.setVerticalGroup(vGroup); 137 | 138 | gameSelectionPanel.setLayout(gameSelectionLayout); 139 | 140 | return gameSelectionPanel; 141 | } 142 | 143 | 144 | /** 145 | * Updates the state of the menu buttons to reflect the given entry. 146 | * 147 | * @param entry 148 | */ 149 | private void updateButtonState(GameListEntry entry) 150 | { 151 | editButton.setEnabled(entry.getGame().isCustomGame()); 152 | deleteButton.setEnabled(entry.getGame().isCustomGame()); 153 | settingsLabel.setText(entry.getGame().getCaption()); 154 | } 155 | 156 | 157 | /** 158 | * Called when the games configuration window is opened to init values. 159 | */ 160 | protected void loadConfigPane() 161 | { 162 | gameList.updateScrollbar(); 163 | } 164 | 165 | 166 | @Override 167 | public void entryCreated(GameListEntry entry) 168 | { 169 | 170 | } 171 | 172 | 173 | @Override 174 | public void entryUpdated(GameListEntry entry) 175 | { 176 | settingsLabel.setText(entry.getGame().getCaption()); 177 | 178 | revalidate(); 179 | repaint(); 180 | } 181 | 182 | 183 | @Override 184 | public void entryDeleted(GameListEntry entry) 185 | { 186 | 187 | } 188 | 189 | 190 | @Override 191 | public void entrySelected(GameListEntry prevEntry, GameListEntry newEntry) 192 | { 193 | if (prevEntry != null) 194 | gameSelectionLayout.replace(prevEntry.getConfigPanel(), newEntry.getConfigPanel()); 195 | 196 | updateButtonState(newEntry); 197 | 198 | revalidate(); 199 | repaint(); 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/settings/GeneralSettingsPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.settings; 2 | 3 | 4 | import java.awt.Component; 5 | import java.awt.event.ItemEvent; 6 | 7 | import javax.swing.BorderFactory; 8 | import javax.swing.Box; 9 | import javax.swing.GroupLayout; 10 | import javax.swing.GroupLayout.Alignment; 11 | import javax.swing.JCheckBox; 12 | import javax.swing.JComboBox; 13 | import javax.swing.JLabel; 14 | import javax.swing.JPanel; 15 | import javax.swing.SwingUtilities; 16 | import javax.swing.border.EtchedBorder; 17 | import javax.swing.border.TitledBorder; 18 | 19 | import com.github.weisj.darklaf.LafManager; 20 | import com.github.weisj.darklaf.theme.Theme; 21 | import com.soulsspeedruns.organizer.managers.SettingsManager; 22 | 23 | 24 | /** 25 | * GeneralSettingsPanel 26 | *

27 | * Contains general settings for the user to change. 28 | * 29 | * @author Kahmul (www.twitch.tv/kahmul78) 30 | * @date 3 Jun 2016 31 | */ 32 | public class GeneralSettingsPanel extends JPanel 33 | { 34 | 35 | private JCheckBox alwaysOnTopCheckbox; 36 | private JCheckBox hotkeysCheckbox; 37 | private JCheckBox doubleClickLoadCheckbox; 38 | private JCheckBox checkForUpdatesCheckbox; 39 | private JCheckBox compactModeCheckbox; 40 | private JComboBox themeCombobox; 41 | 42 | 43 | /** 44 | * Creates a new general settings panel. 45 | */ 46 | protected GeneralSettingsPanel() 47 | { 48 | GroupLayout layout = new GroupLayout(this); 49 | layout.setAutoCreateGaps(true); 50 | layout.setAutoCreateContainerGaps(true); 51 | 52 | Component glue = Box.createHorizontalGlue(); 53 | 54 | JLabel themeLabel = new JLabel("Theme:"); 55 | themeCombobox = createThemeComboBox(); 56 | 57 | JLabel alwaysOnTopLabel = new JLabel("Always On Top:"); 58 | alwaysOnTopLabel.setToolTipText("Forces the organizer to always stay visible over other windows."); 59 | alwaysOnTopCheckbox = new JCheckBox("", SettingsManager.isAlwaysOnTop()); 60 | 61 | JLabel hotkeysLabel = new JLabel("Global Hotkeys:"); 62 | hotkeysLabel.setToolTipText("Enables the global hotkeys below."); 63 | hotkeysCheckbox = new JCheckBox("", SettingsManager.areGlobalHotkeysEnabled()); 64 | 65 | JLabel doubleClickLoadLabel = new JLabel("Allow Double Click To Load Savestates:"); 66 | doubleClickLoadLabel.setToolTipText("Double clicking a savestate in the list will load it."); 67 | doubleClickLoadCheckbox = new JCheckBox("", SettingsManager.isDoubleClickLoadEnabled()); 68 | 69 | JLabel checkForUpdatesLabel = new JLabel("Check For Updates:"); 70 | checkForUpdatesLabel.setToolTipText("Checks for updates to the organizer and notifies you in the bottom right about it."); 71 | checkForUpdatesCheckbox = new JCheckBox("", SettingsManager.isCheckForUpdatesEnabled()); 72 | 73 | JLabel compactModeLabel = new JLabel("Compact Mode:"); 74 | compactModeLabel.setToolTipText("Reduces the size of buttons in the main window and allows a smaller window size."); 75 | compactModeCheckbox = new JCheckBox("", SettingsManager.isCompactModeEnabled()); 76 | 77 | // Horizontal 78 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 79 | 80 | hGroup.addGroup(layout.createParallelGroup().addComponent(hotkeysLabel).addComponent(alwaysOnTopLabel).addComponent(doubleClickLoadLabel).addComponent(checkForUpdatesLabel).addComponent(compactModeLabel).addComponent(themeLabel)); 81 | hGroup.addGroup(layout.createParallelGroup().addComponent(glue)); 82 | hGroup.addGroup(layout.createParallelGroup(Alignment.TRAILING).addComponent(hotkeysCheckbox).addComponent(alwaysOnTopCheckbox).addComponent(doubleClickLoadCheckbox).addComponent(checkForUpdatesCheckbox).addComponent(compactModeCheckbox).addComponent(themeCombobox)); 83 | 84 | layout.setHorizontalGroup(hGroup); 85 | 86 | // Vertical 87 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 88 | 89 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(glue)); 90 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(themeLabel).addComponent(themeCombobox)); 91 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(checkForUpdatesLabel).addComponent(checkForUpdatesCheckbox)); 92 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(alwaysOnTopLabel).addComponent(alwaysOnTopCheckbox)); 93 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(hotkeysLabel).addComponent(hotkeysCheckbox)); 94 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(compactModeLabel).addComponent(compactModeCheckbox)); 95 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(doubleClickLoadLabel).addComponent(doubleClickLoadCheckbox)); 96 | 97 | layout.setVerticalGroup(vGroup); 98 | 99 | setLayout(layout); 100 | TitledBorder border = BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), "General"); 101 | setBorder(border); 102 | } 103 | 104 | 105 | private JComboBox createThemeComboBox() 106 | { 107 | JComboBox themeCombobox = new JComboBox<>(LafManager.getThemeComboBoxModel()); 108 | themeCombobox.setSelectedItem(LafManager.getInstalledTheme()); 109 | 110 | themeCombobox.addItemListener(event -> { 111 | if (event.getStateChange() == ItemEvent.SELECTED) 112 | { 113 | Theme selectedTheme = (Theme) event.getItem(); 114 | if(!LafManager.getInstalledTheme().equals(selectedTheme)) 115 | { 116 | SwingUtilities.invokeLater(() -> { 117 | LafManager.install(selectedTheme); 118 | }); 119 | SettingsManager.setStoredTheme(selectedTheme); 120 | } 121 | 122 | } 123 | }); 124 | 125 | return themeCombobox; 126 | } 127 | 128 | 129 | /** 130 | * Applies the changes to the settings to the program. 131 | */ 132 | protected void applyChanges() 133 | { 134 | SettingsManager.setGlobalHotkeysEnabled(hotkeysCheckbox.isSelected()); 135 | SettingsManager.setAlwaysOnTop(alwaysOnTopCheckbox.isSelected()); 136 | SettingsManager.setDoubleClickLoadEnabled(doubleClickLoadCheckbox.isSelected()); 137 | SettingsManager.setCheckForUpdatesEnabled(checkForUpdatesCheckbox.isSelected()); 138 | SettingsManager.setCompactModeEnabled(compactModeCheckbox.isSelected()); 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/messages/AbstractMessage.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.messages; 2 | 3 | 4 | import java.awt.AlphaComposite; 5 | import java.awt.Color; 6 | import java.awt.Font; 7 | import java.awt.FontMetrics; 8 | import java.awt.Graphics; 9 | import java.awt.Graphics2D; 10 | import java.awt.RenderingHints; 11 | import java.util.Timer; 12 | import java.util.TimerTask; 13 | 14 | import javax.swing.Icon; 15 | import javax.swing.JPanel; 16 | 17 | 18 | /** 19 | * AbstractMessage. 20 | *

21 | * The abstract superclass for all message implementations. 22 | * 23 | * @author Kahmul (www.twitch.tv/kahmul78) 24 | * @date 15 Jul 2017 25 | */ 26 | public abstract class AbstractMessage extends JPanel 27 | { 28 | 29 | private static final Font FONT = new Font("Calibri", Font.BOLD, 17); 30 | private static final int TIME_OUT = 2500; 31 | private static final int iconTextGap = 5; 32 | 33 | public static final AbstractMessage SUCCESSFUL_DELETE = new SuccessfulDeleteMessage(); 34 | public static final AbstractMessage SUCCESSFUL_IMPORT = new SuccessfulImportMessage(); 35 | public static final AbstractMessage SUCCESSFUL_LOAD = new SuccessfulLoadMessage(); 36 | public static final AbstractMessage SUCCESSFUL_REPLACE = new SuccessfulReplaceMessage(); 37 | public static final AbstractMessage SUCCESSFUL_REFRESH = new SuccessfulRefreshMessage(); 38 | 39 | public static final AbstractMessage FAILED_LOAD = new FailedLoadMessage(); 40 | 41 | // used to prevent fadeout when redisplaying a currently displayed message 42 | private static AbstractMessage currentMessage; 43 | 44 | private UndecoratedMessageDialog dialog = null; 45 | 46 | private float alpha = 0.0f; 47 | private float fadeInOutRate = 0.15f; 48 | 49 | private boolean fadingOut = false; 50 | 51 | private Timer fadeOutTimer = new Timer(); 52 | 53 | 54 | protected AbstractMessage() 55 | { 56 | setBackground(new Color(0, 0, 0, 0)); 57 | } 58 | 59 | 60 | @Override 61 | public void paintComponent(Graphics g) 62 | { 63 | super.paintComponent(g); 64 | Graphics2D g2d = (Graphics2D) g; 65 | 66 | // set the opacity 67 | g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, alpha)); 68 | g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 69 | g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 70 | 71 | g.setColor(getColor()); 72 | g.drawRect(0, 0, getWidth() - 1, getHeight() - 1); 73 | g.setFont(FONT); 74 | drawMessageAndIcon(g); 75 | fadeInOut(); 76 | } 77 | 78 | 79 | /** 80 | * Adjusts the alpha value depending on whether the message is in fade-in or fade-out mode. 81 | */ 82 | private void fadeInOut() 83 | { 84 | if (fadingOut) 85 | { 86 | alpha -= fadeInOutRate; 87 | if (alpha <= 0.0f) 88 | { 89 | alpha = 0.0f; 90 | dialog.setVisible(false); 91 | fadingOut = false; 92 | return; 93 | } 94 | repaint(); 95 | return; 96 | } 97 | alpha += fadeInOutRate; 98 | if (alpha >= 1.0f) 99 | { 100 | alpha = 1.0f; 101 | return; 102 | } 103 | repaint(); 104 | } 105 | 106 | 107 | /** 108 | * Draws the message and icon associated with this message. 109 | * 110 | * @param g the graphics to draw on 111 | */ 112 | private void drawMessageAndIcon(Graphics g) 113 | { 114 | FontMetrics metrics = g.getFontMetrics(g.getFont()); 115 | Icon icon = getIcon(); 116 | 117 | int iconX = getWidth() / 2 - metrics.stringWidth(getMessage()) / 2 - icon.getIconWidth()/2 - iconTextGap; 118 | int iconY = getHeight() / 2 - icon.getIconHeight() / 2; 119 | icon.paintIcon(null, g, iconX, iconY); 120 | 121 | int messageX = getWidth() / 2 - metrics.stringWidth(getMessage()) / 2 + icon.getIconWidth()/2 + iconTextGap; 122 | int messageY = getHeight() / 2 + metrics.getAscent() / 2; 123 | g.drawString(getMessage(), messageX, messageY); 124 | } 125 | 126 | 127 | /** 128 | * Starts the fade-out. Is the fade-out complete, the parent dialog is set invisible. 129 | */ 130 | protected void fadeOut() 131 | { 132 | // don't fadeout the current message 133 | if (this.equals(currentMessage)) 134 | return; 135 | fadingOut = true; 136 | repaint(); 137 | } 138 | 139 | 140 | /** 141 | * Starts the fade-in. 142 | */ 143 | protected void fadeIn() 144 | { 145 | fadingOut = false; 146 | repaint(); 147 | } 148 | 149 | 150 | /** 151 | * Starts the fade-in of the message, stops any ongoing fade-out, and resets the fade-out timer. 152 | */ 153 | private void display() 154 | { 155 | fadingOut = false; 156 | fadeOutTimer.cancel(); 157 | fadeOutTimer = new Timer(); 158 | fadeOutTimer.schedule(new TimerTask() { 159 | 160 | @Override 161 | public void run() 162 | { 163 | fadingOut = true; 164 | repaint(); 165 | } 166 | }, TIME_OUT); 167 | if (alpha > 0.0f) 168 | return; 169 | dialog = new UndecoratedMessageDialog(this); 170 | dialog.fadeIn(); 171 | } 172 | 173 | 174 | /** 175 | * Display the given message on screen. 176 | * 177 | * @param message the message to display 178 | */ 179 | public static void display(AbstractMessage message) 180 | { 181 | currentMessage = message; 182 | 183 | SUCCESSFUL_DELETE.fadeOut(); 184 | SUCCESSFUL_IMPORT.fadeOut(); 185 | SUCCESSFUL_LOAD.fadeOut(); 186 | SUCCESSFUL_REPLACE.fadeOut(); 187 | SUCCESSFUL_REFRESH.fadeOut(); 188 | FAILED_LOAD.fadeOut(); 189 | 190 | message.display(); 191 | } 192 | 193 | 194 | /** 195 | * Clears all displayed messages on the screen. 196 | */ 197 | public static void clearAllMessages() 198 | { 199 | currentMessage = null; 200 | 201 | SUCCESSFUL_DELETE.fadeOut(); 202 | SUCCESSFUL_IMPORT.fadeOut(); 203 | SUCCESSFUL_LOAD.fadeOut(); 204 | SUCCESSFUL_REPLACE.fadeOut(); 205 | SUCCESSFUL_REFRESH.fadeOut(); 206 | FAILED_LOAD.fadeOut(); 207 | } 208 | 209 | 210 | /** 211 | * The text associated with this message. 212 | * 213 | * @return the text 214 | */ 215 | protected abstract String getMessage(); 216 | 217 | 218 | /** 219 | * The icon associated with this message. 220 | * 221 | * @return the icon 222 | */ 223 | protected abstract Icon getIcon(); 224 | 225 | 226 | /** 227 | * The size of the icon for this message. 228 | * 229 | * @return 230 | */ 231 | protected abstract int getIconSize(); 232 | 233 | 234 | /** 235 | * The color associated with this message. 236 | * 237 | * @return the color 238 | */ 239 | protected abstract Color getColor(); 240 | 241 | 242 | 243 | } 244 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameConfigProfilesPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import java.awt.GridLayout; 5 | 6 | import javax.swing.GroupLayout; 7 | import javax.swing.GroupLayout.Alignment; 8 | import javax.swing.JButton; 9 | import javax.swing.JLabel; 10 | import javax.swing.JPanel; 11 | import javax.swing.JScrollPane; 12 | 13 | import com.github.weisj.darklaf.components.OverlayScrollPane; 14 | import com.soulsspeedruns.organizer.games.Game; 15 | import com.soulsspeedruns.organizer.games.Profile; 16 | import com.soulsspeedruns.organizer.listeners.ProfileListener; 17 | import com.soulsspeedruns.organizer.managers.GamesManager; 18 | 19 | 20 | /** 21 | * Profile part of the configuration panel. 22 | *

23 | * Contains the profile list as well as the buttons to edit it. 24 | * 25 | * @author Kahmul (www.twitch.tv/kahmul78) 26 | * @date 28 Sep 2015 27 | */ 28 | public class GameConfigProfilesPanel extends JPanel 29 | { 30 | 31 | /** 32 | * Creates a new profile panel. 33 | * 34 | * @param game the game of this panel 35 | */ 36 | protected GameConfigProfilesPanel(Game game) 37 | { 38 | GroupLayout layout = new GroupLayout(this); 39 | layout.setAutoCreateGaps(true); 40 | layout.setAutoCreateContainerGaps(true); 41 | 42 | JLabel profileLabel = new JLabel("Profiles: "); 43 | 44 | ProfileList profileList = createProfileList(game); 45 | OverlayScrollPane listPane = new OverlayScrollPane(profileList, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, 46 | JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 47 | 48 | JPanel optionsPanel = createOptionsPanel(profileList); 49 | 50 | // Horizontal 51 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 52 | 53 | hGroup.addGroup(layout.createParallelGroup().addComponent(profileLabel).addComponent(listPane)); 54 | hGroup.addGroup(layout.createParallelGroup().addComponent(optionsPanel, GroupLayout.PREFERRED_SIZE, new JButton("Browse").getPreferredSize().width, 55 | GroupLayout.PREFERRED_SIZE)); 56 | 57 | layout.setHorizontalGroup(hGroup); 58 | 59 | // Vertical 60 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 61 | 62 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(profileLabel)); 63 | vGroup.addGroup(layout.createParallelGroup(Alignment.BASELINE).addComponent(listPane).addComponent(optionsPanel, GroupLayout.PREFERRED_SIZE, 64 | GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)); 65 | 66 | layout.setVerticalGroup(vGroup); 67 | 68 | setLayout(layout); 69 | } 70 | 71 | 72 | /** 73 | * Creates the profile list. 74 | * 75 | * @return the profile list 76 | */ 77 | private ProfileList createProfileList(Game game) 78 | { 79 | ProfileList profileList = new ProfileList(game); 80 | return profileList; 81 | } 82 | 83 | 84 | /** 85 | * Creates the button panel. 86 | * 87 | * @return the panel containing the buttons 88 | */ 89 | private JPanel createOptionsPanel(ProfileList profileList) 90 | { 91 | JPanel buttonPanel = new JPanel(); 92 | buttonPanel.setLayout(new GridLayout(4, 1)); 93 | 94 | JButton newButton = createNewButton(profileList); 95 | JButton importButton = createImportButton(profileList); 96 | JButton editButton = createEditButton(profileList); 97 | JButton deleteButton = createDeleteButton(profileList); 98 | 99 | buttonPanel.add(newButton); 100 | buttonPanel.add(editButton); 101 | buttonPanel.add(importButton); 102 | buttonPanel.add(deleteButton); 103 | 104 | addButtonListeners(newButton, editButton, importButton, deleteButton); 105 | 106 | return buttonPanel; 107 | } 108 | 109 | 110 | private void addButtonListeners(JButton newButton, JButton editButton, JButton importButton, JButton deleteButton) 111 | { 112 | // TODO Auto-generated method stub 113 | 114 | GamesManager.addProfileListener(new ProfileListener() 115 | { 116 | 117 | @Override 118 | public void profileDeleted(Profile profile) 119 | { 120 | } 121 | 122 | 123 | @Override 124 | public void profileCreated(Profile profile) 125 | { 126 | } 127 | 128 | 129 | @Override 130 | public void profileDirectoryChanged(Game game) 131 | { 132 | newButton.setEnabled(game.getDirectory() != null); 133 | editButton.setEnabled(game.getDirectory() != null); 134 | importButton.setEnabled(game.getDirectory() != null); 135 | deleteButton.setEnabled(game.getDirectory() != null); 136 | } 137 | 138 | 139 | @Override 140 | public void changedToProfile(Profile profile) 141 | { 142 | } 143 | 144 | 145 | @Override 146 | public void changedToGame(Game game) 147 | { 148 | } 149 | 150 | }); 151 | } 152 | 153 | 154 | /** 155 | * Creates the 'Import' button. 156 | * 157 | * @return the import button 158 | */ 159 | private JButton createImportButton(ProfileList profileList) 160 | { 161 | JButton importButton = new JButton("Import"); 162 | importButton.setToolTipText("Import profile(s)."); 163 | importButton.setEnabled(profileList.getGame().getDirectory() != null); 164 | importButton.addActionListener(event -> { 165 | profileList.askToImportProfiles(); 166 | }); 167 | 168 | return importButton; 169 | } 170 | 171 | 172 | /** 173 | * Creates the 'New' button. 174 | * 175 | * @param game the game of the current panel 176 | * @return the new button 177 | */ 178 | private JButton createNewButton(ProfileList profileList) 179 | { 180 | JButton newButton = new JButton("New"); 181 | newButton.setToolTipText("Create a new profile."); 182 | newButton.setEnabled(profileList.getGame().getDirectory() != null); 183 | newButton.addActionListener(event -> { 184 | profileList.askToCreateProfile(); 185 | }); 186 | return newButton; 187 | } 188 | 189 | 190 | /** 191 | * Creates the 'Edit' button. 192 | * 193 | * @param game the game of the current panel 194 | * @return the edit button 195 | */ 196 | private JButton createEditButton(ProfileList profileList) 197 | { 198 | JButton editButton = new JButton("Edit"); 199 | editButton.setToolTipText("Edit the selected profile."); 200 | editButton.setEnabled(profileList.getGame().getDirectory() != null); 201 | editButton.addActionListener(event -> { 202 | profileList.askToEditProfile(profileList.getSelectedValue()); 203 | }); 204 | return editButton; 205 | } 206 | 207 | 208 | /** 209 | * Creates the 'Delete' button. 210 | * 211 | * @param game the game of the current panel 212 | * @return the delete button 213 | */ 214 | private JButton createDeleteButton(ProfileList profileList) 215 | { 216 | JButton deleteButton = new JButton("Delete"); 217 | deleteButton.setToolTipText("Delete the selected profile."); 218 | deleteButton.setEnabled(profileList.getGame().getDirectory() != null); 219 | deleteButton.addActionListener(event -> { 220 | profileList.askToDeleteProfiles(profileList.getSelectedValuesList()); 221 | }); 222 | return deleteButton; 223 | } 224 | 225 | } 226 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/managers/VersionManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | package com.soulsspeedruns.organizer.managers; 5 | 6 | 7 | import java.io.BufferedReader; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.io.Reader; 12 | import java.net.URI; 13 | import java.nio.charset.StandardCharsets; 14 | 15 | import org.json.JSONObject; 16 | 17 | 18 | /** 19 | * VersionManager. 20 | *

21 | * Handles methods around the application version and the system environment. 22 | * 23 | * @author Kahmul (www.twitch.tv/kahmul78) 24 | * @date 20 Jan 2024 25 | */ 26 | public class VersionManager 27 | { 28 | 29 | private static final String VERSION = "1.6.0"; 30 | 31 | private static final String LATEST_RELEASE_JSON_URL = "https://api.github.com/repos/Kahmul/SoulsSpeedruns-Save-Organizer/releases/latest"; 32 | private static final String LATEST_RELEASE_URL = "https://github.com/Kahmul/SoulsSpeedruns-Save-Organizer/releases/latest"; 33 | 34 | private static String latestReleaseVersion; 35 | 36 | private static String operatingSystem; 37 | 38 | 39 | protected static void initialize() 40 | { 41 | determineOS(); 42 | } 43 | 44 | 45 | /** 46 | * Determines the general OS name the application is running on. 47 | */ 48 | private static void determineOS() 49 | { 50 | String osName = System.getProperty("os.name").toLowerCase(); 51 | if (osName.contains("win")) 52 | { 53 | operatingSystem = "Windows"; 54 | } 55 | else if (osName.contains("nix") || osName.contains("nux") || osName.contains("aix")) 56 | { 57 | operatingSystem = "Linux"; 58 | } 59 | else if (osName.contains("mac")) 60 | { 61 | operatingSystem = "Mac"; 62 | } 63 | } 64 | 65 | 66 | /** 67 | * Returns the save organizer version. 68 | * 69 | * @return the current version 70 | */ 71 | public static String getVersion() 72 | { 73 | return VERSION; 74 | } 75 | 76 | 77 | /** 78 | * Returns the URL to the latest release on GitHub. 79 | * 80 | * @return latest github release URl 81 | */ 82 | public static String getLatestReleaseURL() 83 | { 84 | return LATEST_RELEASE_URL; 85 | } 86 | 87 | 88 | /** 89 | * Returns whether the OS this application is running on is Windows. 90 | * 91 | * @return whether the OS is Windows 92 | */ 93 | public static boolean isRunningOnWindows() 94 | { 95 | return operatingSystem.contains("Windows"); 96 | } 97 | 98 | 99 | /** 100 | * Returns whether the OS this application is running on is Linux. It is assumed for the Souls games that they are running on a compatibility 101 | * layer such as Proton. 102 | * 103 | * @return whether the OS is Linux 104 | */ 105 | public static boolean isRunningOnLinux() 106 | { 107 | return operatingSystem.contains("Linux"); 108 | } 109 | 110 | 111 | /** 112 | * Gets the major Java version of the current runtime. 113 | * 114 | * @return the currently used major Java version 115 | */ 116 | public static int getMajorJavaVersion() 117 | { 118 | String[] versionElements = System.getProperty("java.version").split("\\."); 119 | int firstElement = Integer.parseInt(versionElements[0]); 120 | int version = firstElement == 1 ? Integer.parseInt(versionElements[1]) : firstElement; 121 | 122 | return version; 123 | } 124 | 125 | 126 | /** 127 | * Checks whether the local Save Organizer version is outdated compared to the latest GitHub release. 128 | * 129 | * @return whether the local version is outdated 130 | */ 131 | public static boolean isVersionOutdated() 132 | { 133 | if (!SettingsManager.isCheckForUpdatesEnabled()) 134 | return false; 135 | 136 | if (latestReleaseVersion == null) 137 | latestReleaseVersion = getLatestReleaseVersion(); 138 | 139 | String[] vals1 = VERSION.split("\\."); 140 | String[] vals2 = latestReleaseVersion.split("\\."); 141 | int i = 0; 142 | // set index to first non-equal ordinal or length of shortest version string 143 | while (i < vals1.length && i < vals2.length && vals1[i].equals(vals2[i])) 144 | { 145 | i++; 146 | } 147 | // compare first non-equal ordinal number 148 | if (i < vals1.length && i < vals2.length) 149 | { 150 | int diff = Integer.valueOf(vals1[i]).compareTo(Integer.valueOf(vals2[i])); 151 | return Integer.signum(diff) == -1; 152 | } 153 | // the strings are equal or one string is a substring of the other 154 | // e.g. "1.2.3" = "1.2.3" or "1.2.3" < "1.2.3.4" 155 | return Integer.signum(vals1.length - vals2.length) == -1; 156 | } 157 | 158 | 159 | /** 160 | * Checks the latest release version on GitHub and returns it. 161 | * 162 | * @return the latest release version on GitHub 163 | */ 164 | public static String getLatestReleaseVersion() 165 | { 166 | JSONObject latestReleaseJSON = getLatestReleaseJSON(); 167 | if (latestReleaseJSON != null) 168 | { 169 | String version = latestReleaseJSON.getString("tag_name"); 170 | String prefix = version.split("[0-9]")[0]; 171 | version = version.substring(prefix.length()); 172 | 173 | return version; 174 | } 175 | return "0.0"; 176 | } 177 | 178 | 179 | /** 180 | * Retrieves the description of the latest release from GitHub. 181 | * 182 | * @return the latest release description 183 | */ 184 | public static String getLatestReleaseDescription() 185 | { 186 | JSONObject latestReleaseJSON = getLatestReleaseJSON(); 187 | if (latestReleaseJSON != null) 188 | return latestReleaseJSON.getString("body"); 189 | return ""; 190 | } 191 | 192 | 193 | /** 194 | * Builds the download URL based on the latest release version. 195 | * 196 | * @return the download URL for the latest release 197 | */ 198 | public static String getLatestReleaseDownloadURL() 199 | { 200 | String latestVersion = getLatestReleaseVersion(); 201 | return "https://github.com/Kahmul/SoulsSpeedruns-Save-Organizer/releases/download/v." + latestVersion + "SoulsSpeedruns.-.Save.Organizer." 202 | + latestVersion + ".zip"; 203 | } 204 | 205 | 206 | /** 207 | * Creates a JSONObject of the latest release on GitHub. 208 | * 209 | * @return the JSONObject of the latest release 210 | */ 211 | private static JSONObject getLatestReleaseJSON() 212 | { 213 | try (InputStream is = URI.create(LATEST_RELEASE_JSON_URL).toURL().openStream()) 214 | { 215 | BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); 216 | String jsonText = readAll(rd); 217 | JSONObject json = new JSONObject(jsonText); 218 | return json; 219 | } 220 | catch (Exception e) 221 | { 222 | return null; 223 | } 224 | } 225 | 226 | 227 | /** 228 | * Reads all the input from a Reader and returns it in a single String. 229 | * 230 | * @param rd the reader 231 | * @return the input 232 | * @throws IOException 233 | */ 234 | private static String readAll(Reader rd) throws IOException 235 | { 236 | StringBuilder sb = new StringBuilder(); 237 | int cp; 238 | while ((cp = rd.read()) != -1) 239 | { 240 | sb.append((char) cp); 241 | } 242 | return sb.toString(); 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/components/ReadOnlyButton.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.components; 2 | 3 | 4 | import java.awt.event.ComponentAdapter; 5 | import java.awt.event.ComponentEvent; 6 | import java.awt.event.MouseEvent; 7 | import java.awt.event.MouseListener; 8 | import java.io.File; 9 | 10 | import javax.swing.JLabel; 11 | 12 | import com.github.weisj.darklaf.LafManager; 13 | import com.github.weisj.darklaf.theme.event.ThemeChangeEvent; 14 | import com.github.weisj.darklaf.theme.event.ThemeChangeListener; 15 | import com.soulsspeedruns.organizer.games.Game; 16 | import com.soulsspeedruns.organizer.games.Profile; 17 | import com.soulsspeedruns.organizer.listeners.ProfileListener; 18 | import com.soulsspeedruns.organizer.listeners.SaveListener; 19 | import com.soulsspeedruns.organizer.listeners.SettingsListener; 20 | import com.soulsspeedruns.organizer.managers.GamesManager; 21 | import com.soulsspeedruns.organizer.managers.IconsAndFontsManager; 22 | import com.soulsspeedruns.organizer.managers.OrganizerManager; 23 | import com.soulsspeedruns.organizer.managers.SavesManager; 24 | import com.soulsspeedruns.organizer.managers.SettingsManager; 25 | import com.soulsspeedruns.organizer.managers.VersionManager; 26 | import com.soulsspeedruns.organizer.savelist.Save; 27 | import com.soulsspeedruns.organizer.savelist.SaveListEntry; 28 | 29 | 30 | /** 31 | * Read Only Button. 32 | *

33 | * Button to make the given file read-only and vice-versa. 34 | * 35 | * @author Kahmul (www.twitch.tv/kahmul78) 36 | * @date 26 Sep 2015 37 | */ 38 | public class ReadOnlyButton extends JLabel implements MouseListener, ProfileListener, SaveListener, SettingsListener 39 | { 40 | 41 | private File file; 42 | 43 | 44 | /** 45 | * Creates a new read only button with the given file and image. 46 | * 47 | * @param file the file to associate with this button. 48 | * @param isCompact whether to show the "writable/read-only" text 49 | */ 50 | public ReadOnlyButton(File file) 51 | { 52 | setFile(file); 53 | 54 | addMouseListener(this); 55 | GamesManager.addProfileListener(this); 56 | SavesManager.addSaveListener(this); 57 | SettingsManager.addSettingsListener(this); 58 | 59 | LafManager.addThemeChangeListener(new ThemeChangeListener() 60 | { 61 | 62 | @Override 63 | public void themeInstalled(ThemeChangeEvent e) 64 | { 65 | refreshAppearance(false); 66 | } 67 | 68 | 69 | @Override 70 | public void themeChanged(ThemeChangeEvent e) 71 | { 72 | } 73 | }); 74 | 75 | OrganizerManager.getMainWindow().addComponentListener(new ComponentAdapter() 76 | { 77 | 78 | public void componentResized(ComponentEvent e) 79 | { 80 | refreshAppearance(false); 81 | } 82 | }); 83 | 84 | setVisible(true); 85 | } 86 | 87 | 88 | /** 89 | * Returns the file that is associated with this button. 90 | * 91 | * @return the file that is associated with this button 92 | */ 93 | public File getFile() 94 | { 95 | return file; 96 | } 97 | 98 | 99 | /** 100 | * Sets the file to associate this button with. 101 | * 102 | * @param file the file to associate this button with 103 | */ 104 | public void setFile(File file) 105 | { 106 | if (file == null || !file.exists()) 107 | { 108 | this.file = null; 109 | setVisible(false); 110 | setToolTipText(null); 111 | return; 112 | } 113 | this.file = file; 114 | setVisible(true); 115 | refreshAppearance(false); 116 | } 117 | 118 | 119 | /** 120 | * Simulates a click. 121 | */ 122 | public void doClick() 123 | { 124 | if (file == null || !file.exists() || !GamesManager.getSelectedGame().supportsReadOnly()) 125 | return; 126 | file.setWritable(!file.canWrite()); 127 | refreshAppearance(true); 128 | } 129 | 130 | 131 | /** 132 | * Changes the image of the read-only button depending on the state of the file and whether the mouse is hovered over it. 133 | * 134 | * @param isHovering 135 | */ 136 | private void refreshAppearance(boolean isHovering) 137 | { 138 | boolean isWritable = file != null ? file.canWrite() : false; 139 | boolean showText = !SettingsManager.isCompactModeEnabled() 140 | && (!VersionManager.isVersionOutdated() || OrganizerManager.getMainWindow().getWidth() > 700); 141 | if (isWritable) 142 | { 143 | setText(showText ? "Writable" : null); 144 | setIcon(IconsAndFontsManager.getWritableIcon(IconsAndFontsManager.ICON_SIZE_LARGE, isHovering)); 145 | setToolTipText("Click to turn on read-only for the game's savefile."); 146 | } 147 | else 148 | { 149 | setText(showText ? "Read-Only" : null); 150 | setIcon(IconsAndFontsManager.getReadOnlyIcon(IconsAndFontsManager.ICON_SIZE_LARGE, isHovering)); 151 | setToolTipText("Click to turn off read-only for the game's savefile."); 152 | } 153 | 154 | } 155 | 156 | 157 | @Override 158 | public void setVisible(boolean flag) 159 | { 160 | super.setVisible(flag); 161 | 162 | if (flag) 163 | { 164 | if (!GamesManager.getSelectedGame().supportsReadOnly()) 165 | super.setVisible(false); 166 | if (!GamesManager.isAProfileSelected()) 167 | super.setVisible(false); 168 | } 169 | 170 | } 171 | 172 | 173 | @Override 174 | public void mouseClicked(MouseEvent e) 175 | { 176 | doClick(); 177 | } 178 | 179 | 180 | @Override 181 | public void mouseEntered(MouseEvent e) 182 | { 183 | refreshAppearance(true); 184 | } 185 | 186 | 187 | @Override 188 | public void mouseExited(MouseEvent e) 189 | { 190 | refreshAppearance(false); 191 | } 192 | 193 | 194 | @Override 195 | public void mousePressed(MouseEvent e) 196 | { 197 | } 198 | 199 | 200 | @Override 201 | public void mouseReleased(MouseEvent e) 202 | { 203 | } 204 | 205 | 206 | @Override 207 | public void profileDeleted(Profile profile) 208 | { 209 | } 210 | 211 | 212 | @Override 213 | public void profileCreated(Profile profile) 214 | { 215 | } 216 | 217 | 218 | @Override 219 | public void profileDirectoryChanged(Game game) 220 | { 221 | setFile(game.getSaveFileLocation()); 222 | if (file == null || !file.exists() || !game.supportsReadOnly()) 223 | setVisible(false); 224 | else 225 | setVisible(true); 226 | } 227 | 228 | 229 | @Override 230 | public void changedToProfile(Profile profile) 231 | { 232 | } 233 | 234 | 235 | @Override 236 | public void changedToGame(Game game) 237 | { 238 | setFile(game.getSaveFileLocation()); 239 | if (file == null || !file.exists() || !game.supportsReadOnly()) 240 | setVisible(false); 241 | else 242 | setVisible(true); 243 | } 244 | 245 | 246 | @Override 247 | public void entryCreated(SaveListEntry entry) 248 | { 249 | } 250 | 251 | 252 | @Override 253 | public void entryRenamed(SaveListEntry entry) 254 | { 255 | } 256 | 257 | 258 | @Override 259 | public void entrySelected(SaveListEntry entry) 260 | { 261 | } 262 | 263 | 264 | @Override 265 | public void saveLoadStarted(Save save) 266 | { 267 | } 268 | 269 | 270 | @Override 271 | public void saveLoadFinished(Save save) 272 | { 273 | refreshAppearance(false); 274 | } 275 | 276 | 277 | @Override 278 | public void gameFileWritableStateChanged(boolean writeable) 279 | { 280 | refreshAppearance(false); 281 | } 282 | 283 | 284 | @Override 285 | public void settingChanged(String prefsKey) 286 | { 287 | refreshAppearance(false); 288 | } 289 | 290 | } 291 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | SoulsSpeedruns 5 | SoulsSpeedrunsSaveOrganizer 6 | 1.6.0.0 7 | SoulsSpeedruns - Save Organizer 8 | SoulsSpeedruns - Save Organizer 9 | 10 | 11 | 16 | 17 | com.1stleg 18 | jnativehook 19 | 2.0.3 20 | 21 | 26 | 27 | com.github.jiconfont 28 | jiconfont-swing 29 | 1.0.0 30 | 31 | 32 | com.github.jiconfont 33 | jiconfont-font_awesome 34 | 4.7.0.0 35 | 36 | 41 | 42 | com.github.jiconfont 43 | jiconfont-elusive 44 | 2.0.1 45 | 46 | 51 | 52 | com.github.jiconfont 53 | jiconfont-open_iconic 54 | 1.1.1.1 55 | 56 | 61 | 62 | org.json 63 | json 64 | 20170516 65 | 66 | 67 | com.github.weisj 68 | darklaf-core 69 | 3.0.2 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | src 79 | 80 | 81 | src 82 | 83 | **/*.java 84 | 85 | 86 | 87 | 88 | 89 | maven-compiler-plugin 90 | 3.12.0 91 | 92 | 1.8 93 | 1.8 94 | 95 | 96 | 97 | maven-assembly-plugin 98 | 99 | 100 | jar-with-dependencies 101 | 102 | 103 | 104 | com.soulsspeedruns.organizer.managers.OrganizerManager 105 | 106 | 107 | 108 | 109 | 110 | package 111 | 112 | single 113 | 114 | 115 | 116 | 117 | 118 | 119 | com.akathist.maven.plugins.launch4j 120 | launch4j-maven-plugin 121 | 2.4.1 122 | 123 | 124 | l4j-gui 125 | package 126 | launch4j 127 | 128 | gui 129 | target/${project.name}.exe 130 | src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo64.ico 131 | target/soulsspeedrunssaveorganizer-${project.version}-jar-with-dependencies.jar 132 | ${project.name} 133 | 134 | com.soulsspeedruns.organizer.managers.OrganizerManager 135 | 136 | 137 | JRE path (%VAR%) 138 | 1.8 139 | 140 | 141 | src/com/soulsspeedruns/organizer/images/Splash_Screen.bmp 142 | true 143 | 10 144 | true 145 | 146 | 147 | ${project.version} 148 | ${project.version} 149 | ${project.name} 150 | N/A 151 | ${project.version} 152 | ${project.version} 153 | ${project.name} 154 | ${project.name} 155 | ${project.name}.exe 156 | 157 | 158 | 159 | 160 | l4j-gui-portable 161 | package 162 | launch4j 163 | 164 | gui 165 | target/${project.name} - Bundled.exe 166 | src/com/soulsspeedruns/organizer/images/SoulsSpeedrunsLogo64.ico 167 | target/soulsspeedrunssaveorganizer-${project.version}-jar-with-dependencies.jar 168 | ${project.name} 169 | 170 | com.soulsspeedruns.organizer.managers.OrganizerManager 171 | 172 | 173 | ./jre 174 | 175 | 176 | src/com/soulsspeedruns/organizer/images/Splash_Screen.bmp 177 | true 178 | 10 179 | true 180 | 181 | 182 | ${project.version} 183 | ${project.version} 184 | ${project.name} 185 | N/A 186 | ${project.version} 187 | ${project.version} 188 | ${project.name} 189 | ${project.name} 190 | ${project.name}.exe 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameConfigDirectoryPanel.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import java.io.File; 5 | 6 | import javax.swing.GroupLayout; 7 | import javax.swing.GroupLayout.Alignment; 8 | import javax.swing.JButton; 9 | import javax.swing.JFileChooser; 10 | import javax.swing.JLabel; 11 | import javax.swing.JOptionPane; 12 | import javax.swing.JPanel; 13 | import javax.swing.JTextField; 14 | import javax.swing.SwingUtilities; 15 | 16 | import com.github.weisj.darklaf.ui.text.DarkTextUI; 17 | import com.soulsspeedruns.organizer.games.Game; 18 | import com.soulsspeedruns.organizer.managers.GamesManager; 19 | import com.soulsspeedruns.organizer.managers.SettingsManager; 20 | 21 | 22 | /** 23 | * Directory part of the configuration window. 24 | *

25 | * Contains the directory field as well as the browse button. 26 | * 27 | * @author Kahmul (www.twitch.tv/kahmul78) 28 | * @date 28 Sep 2015 29 | */ 30 | public class GameConfigDirectoryPanel extends JPanel 31 | { 32 | 33 | /** 34 | * Creates a new directory panel. 35 | * 36 | * @param game the game of this panel 37 | */ 38 | protected GameConfigDirectoryPanel(Game game) 39 | { 40 | GroupLayout layout = new GroupLayout(this); 41 | layout.setAutoCreateGaps(true); 42 | layout.setAutoCreateContainerGaps(true); 43 | 44 | File saveFile = game.getSaveFileLocation(); 45 | File gameDir = game.getDirectory(); 46 | 47 | JLabel saveFileLabel = new JLabel("Savefile Location:"); 48 | JLabel directoryLabel = new JLabel("Profiles Directory:"); 49 | 50 | JTextField saveFileField = createSaveFileTextField(saveFile, game); 51 | JTextField directoryField = new JTextField(gameDir != null ? gameDir.getPath() : ""); 52 | 53 | JButton directoryBrowseButton = createDirectoryBrowseButton(directoryField, game); 54 | JButton saveFileBrowseButton = createSaveFileBrowseButton(saveFileField, directoryField, directoryBrowseButton, game); 55 | 56 | saveFileField.setEditable(false); 57 | directoryField.setEditable(false); 58 | 59 | // Horizontal 60 | GroupLayout.SequentialGroup hGroup = layout.createSequentialGroup(); 61 | 62 | hGroup.addGroup(layout.createParallelGroup().addComponent(saveFileLabel).addComponent(saveFileField).addComponent(directoryLabel) 63 | .addComponent(directoryField)); 64 | hGroup.addGroup(layout.createParallelGroup() 65 | .addComponent(saveFileBrowseButton, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE) 66 | .addComponent(directoryBrowseButton, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)); 67 | 68 | layout.setHorizontalGroup(hGroup); 69 | 70 | // Vertical 71 | GroupLayout.SequentialGroup vGroup = layout.createSequentialGroup(); 72 | 73 | vGroup.addGroup(layout.createParallelGroup(Alignment.CENTER).addComponent(saveFileLabel)); 74 | 75 | vGroup.addGroup(layout.createParallelGroup(Alignment.CENTER).addComponent(saveFileField).addComponent(saveFileBrowseButton)); 76 | 77 | vGroup.addGroup(layout.createParallelGroup(Alignment.CENTER).addComponent(directoryLabel)); 78 | 79 | vGroup.addGroup(layout.createParallelGroup(Alignment.CENTER).addComponent(directoryField).addComponent(directoryBrowseButton)); 80 | 81 | layout.setVerticalGroup(vGroup); 82 | 83 | setLayout(layout); 84 | } 85 | 86 | 87 | private JTextField createSaveFileTextField(File saveFile, Game game) 88 | { 89 | JTextField saveFileField = new JTextField(saveFile != null ? saveFile.getPath() : ""); 90 | saveFileField.putClientProperty(DarkTextUI.KEY_DEFAULT_TEXT, game.getSuggestedSaveLocation()); 91 | 92 | return saveFileField; 93 | } 94 | 95 | 96 | /** 97 | * @param saveFileField 98 | * @param game 99 | * @return 100 | */ 101 | private JButton createSaveFileBrowseButton(JTextField saveFileField, JTextField directoryField, JButton directoryBrowseButton, Game game) 102 | { 103 | JButton browseButton = new JButton("Browse"); 104 | 105 | browseButton.addActionListener(event -> { 106 | JFileChooser fc = new JFileChooser(game.getSaveFilePathOrSuggested()); 107 | fc.setSelectedFile(new File(game.getSaveFilePathOrSuggested() + File.separator + game.getSaveName())); 108 | fc.setFileSelectionMode(JFileChooser.FILES_ONLY); 109 | int val = fc.showOpenDialog(SwingUtilities.windowForComponent(this)); 110 | if (val == JFileChooser.APPROVE_OPTION) 111 | { 112 | File selectedSavefile = fc.getSelectedFile(); 113 | if (selectedSavefile == null || !selectedSavefile.exists()) 114 | { 115 | JOptionPane.showMessageDialog(null, "This file doesn't exist!", "Error occurred", JOptionPane.ERROR_MESSAGE); 116 | return; 117 | } 118 | if (selectedSavefile.getName().equalsIgnoreCase(game.getSaveName())) 119 | { 120 | game.setSaveFileLocation(selectedSavefile); 121 | saveFileField.setText(selectedSavefile.getPath()); 122 | directoryBrowseButton.setEnabled(true); 123 | int confirm = JOptionPane 124 | .showConfirmDialog(getParent(), 125 | "Do you wish to use the directory of this savefile to store the profiles for this game?" 126 | + " You can choose an alternative directory if you wish.", 127 | "Choosing Savefile", JOptionPane.YES_NO_OPTION); 128 | if (confirm == 0) 129 | { 130 | directoryField.setText(selectedSavefile.getParentFile().getPath()); 131 | game.setDirectory(selectedSavefile.getParentFile()); 132 | SettingsManager.storeGameProperties(game); 133 | GamesManager.fireProfileDirectoryChangedEvent(game); 134 | return; 135 | } 136 | SettingsManager.storeGameProperties(game); 137 | return; 138 | } 139 | JOptionPane.showMessageDialog(null, "Filename needs to be '" + game.getSaveName() + "'!", "Error occurred", 140 | JOptionPane.ERROR_MESSAGE); 141 | } 142 | }); 143 | return browseButton; 144 | } 145 | 146 | 147 | /** 148 | * Creates the browse button. 149 | * 150 | * @param directoryField the directory field of this panel 151 | * @param game the game associated with this panel 152 | * @return the browse button 153 | */ 154 | private JButton createDirectoryBrowseButton(JTextField directoryField, Game game) 155 | { 156 | JButton browseButton = new JButton("Browse"); 157 | browseButton.setEnabled(game.getSaveFileLocation() != null); 158 | 159 | browseButton.addActionListener(event -> { 160 | if (game.getSaveFileLocation() == null) 161 | { 162 | JOptionPane.showMessageDialog(null, "Choose a savefile location first before deciding on a profile directory!", "Warning", 163 | JOptionPane.WARNING_MESSAGE); 164 | return; 165 | } 166 | JFileChooser fc = new JFileChooser(directoryField.getText()); 167 | fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 168 | int val = fc.showOpenDialog(SwingUtilities.windowForComponent(this)); 169 | if (val == JFileChooser.APPROVE_OPTION) 170 | { 171 | File selectedDir = fc.getSelectedFile(); 172 | if (selectedDir == null || !selectedDir.exists()) 173 | { 174 | JOptionPane.showMessageDialog(null, "This directory doesn't exist!", "Error occurred", JOptionPane.ERROR_MESSAGE); 175 | return; 176 | } 177 | directoryField.setText(selectedDir.getPath()); 178 | game.setDirectory(selectedDir); 179 | SettingsManager.storeGameProperties(game); 180 | GamesManager.fireProfileDirectoryChangedEvent(game); 181 | } 182 | }); 183 | return browseButton; 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/games/config/GameList.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.games.config; 2 | 3 | 4 | import java.awt.Point; 5 | import java.awt.Rectangle; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import javax.swing.BoxLayout; 10 | import javax.swing.JOptionPane; 11 | import javax.swing.JPanel; 12 | import javax.swing.JScrollPane; 13 | import javax.swing.SwingUtilities; 14 | import javax.swing.TransferHandler; 15 | import javax.swing.UIManager; 16 | import javax.swing.border.MatteBorder; 17 | 18 | import com.soulsspeedruns.organizer.games.Game; 19 | import com.soulsspeedruns.organizer.managers.GamesManager; 20 | import com.soulsspeedruns.organizer.managers.SettingsManager; 21 | 22 | 23 | /** 24 | * Game List 25 | *

26 | * A scrollable list of games. 27 | * 28 | * @author Kahmul (www.twitch.tv/kahmul78) 29 | * @date 15 Jan 2024 30 | */ 31 | public class GameList extends JScrollPane 32 | { 33 | 34 | private final List entries = new ArrayList<>(); 35 | private GameListEntry selectedEntry; 36 | private GameListEntry dropTarget; 37 | 38 | private JPanel listPanel; 39 | 40 | private List listeners = new ArrayList<>(); 41 | 42 | private final TransferHandler transferHandler = new GameListTransferHandler(this); 43 | 44 | 45 | public GameList() 46 | { 47 | listPanel = new JPanel(); 48 | 49 | listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.PAGE_AXIS)); 50 | 51 | List games = Game.GAMES; 52 | for (Game game : games) 53 | { 54 | GameListEntry entry = new GameListEntry(game, this); 55 | 56 | if (GamesManager.getSelectedGame().equals(game)) 57 | setSelectedEntry(entry); 58 | 59 | entries.add(entry); 60 | listPanel.add(entry); 61 | } 62 | 63 | setViewportView(listPanel); 64 | getVerticalScrollBar().setUnitIncrement(10); 65 | setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 66 | 67 | setBorder(new MatteBorder(0, 0, 0, 1, UIManager.getColor("borderSecondary"))); 68 | 69 | setTransferHandler(transferHandler); 70 | } 71 | 72 | 73 | /** 74 | * Updates the scrollbar to the currently selected entry. Used to move the scrollbar to the selected entry when the window opens. 75 | */ 76 | protected void updateScrollbar() 77 | { 78 | getVerticalScrollBar().setValue(selectedEntry.getLocation().y); 79 | } 80 | 81 | 82 | /** 83 | * Creates a new game along with an entry in the list with the given game name and savefile name 84 | * 85 | * @param gameName the name of the game to add 86 | * @param saveName the game's savefile name 87 | */ 88 | protected void addEntry(String gameName, String saveName) 89 | { 90 | int gameID = GamesManager.getNewCustomGameID(); 91 | if (gameID == -1) 92 | { 93 | JOptionPane.showMessageDialog(SwingUtilities.windowForComponent(this), "Error when saving new custom game. Backing store unavailable.", 94 | "Error", JOptionPane.ERROR_MESSAGE); 95 | return; 96 | } 97 | 98 | Game newGame = Game.createGame(gameName, String.valueOf(gameID), saveName, null, null, true, true); 99 | GameListEntry newGameEntry = new GameListEntry(newGame, this); 100 | entries.add(newGameEntry); 101 | listPanel.add(newGameEntry); 102 | 103 | setSelectedEntry(newGameEntry); 104 | 105 | SettingsManager.storeGameProperties(newGame); 106 | 107 | validate(); 108 | getVerticalScrollBar().setValue(newGameEntry.getLocation().y); 109 | } 110 | 111 | 112 | /** 113 | * Updates the name or savefile name of an existing game. 114 | * 115 | * @param gameName the new game name 116 | * @param saveName the new savefile name 117 | */ 118 | protected void updateEntry(String gameName, String saveName) 119 | { 120 | Game selectedGame = selectedEntry.getGame(); 121 | selectedGame.setCaption(gameName); 122 | selectedGame.setSaveName(saveName); 123 | 124 | selectedEntry.setText(gameName); 125 | 126 | SettingsManager.storeGameProperties(selectedGame); 127 | 128 | fireEntryUpdatedEvent(selectedEntry); 129 | 130 | revalidate(); 131 | repaint(); 132 | } 133 | 134 | 135 | /** 136 | * Deletes the currently selected entry. 137 | */ 138 | protected void deleteSelectedEntry() 139 | { 140 | int selectedIndex = entries.indexOf(selectedEntry); 141 | int newIndex = Math.max(0, entries.indexOf(selectedEntry) - 1); 142 | 143 | entries.remove(selectedIndex); 144 | listPanel.remove(selectedIndex); 145 | 146 | Game.deleteGame(selectedEntry.getGame()); 147 | 148 | setSelectedEntry(entries.get(newIndex)); 149 | } 150 | 151 | 152 | /** 153 | * Get the currently selected entry in this list 154 | * 155 | * @return the selected entry 156 | */ 157 | protected GameListEntry getSelectedEntry() 158 | { 159 | return selectedEntry; 160 | } 161 | 162 | 163 | /** 164 | * Sets the selected entry to the give entry. 165 | * 166 | * @param entry the entry to select 167 | */ 168 | protected void setSelectedEntry(GameListEntry entry) 169 | { 170 | if (entry != selectedEntry) 171 | { 172 | if (selectedEntry != null) 173 | selectedEntry.setSelected(false); 174 | 175 | entry.setSelected(true); 176 | 177 | fireEntrySelectedEvent(selectedEntry, entry); 178 | 179 | selectedEntry = entry; 180 | 181 | revalidate(); 182 | repaint(); 183 | } 184 | } 185 | 186 | 187 | /** 188 | * Clears any potential drop targets for any potentially ongoing drag operations. 189 | */ 190 | protected void clearDropTarget() 191 | { 192 | if (dropTarget != null) 193 | dropTarget.setIsDropTarget(false, false); 194 | dropTarget = null; 195 | } 196 | 197 | 198 | /** 199 | * Highlights the entry at the given index as a potential target to drop another list entry onto. 200 | * 201 | * @param index the index of the drop target 202 | */ 203 | protected void setDropTargetIndex(int index) 204 | { 205 | boolean isEndOfList = false; 206 | if (index < 0) 207 | index = 0; 208 | if (index >= entries.size()) 209 | { 210 | index = entries.size() - 1; 211 | isEndOfList = true; 212 | } 213 | 214 | if (dropTarget != null) 215 | dropTarget.setIsDropTarget(false, isEndOfList); 216 | 217 | dropTarget = entries.get(index); 218 | if (dropTarget != null) 219 | dropTarget.setIsDropTarget(true, isEndOfList); 220 | 221 | } 222 | 223 | 224 | /** 225 | * Returns the drop index of a potential drop operation based on the given point in the list. 226 | * 227 | * @param p the point in the list 228 | * @return the index of a potential drop operation 229 | */ 230 | protected int getDropIndexByPoint(Point p) 231 | { 232 | for (int i = 0; i < entries.size(); i++) 233 | { 234 | Rectangle rect = entries.get(i).getBounds(); 235 | 236 | Rectangle upperHalf = new Rectangle(rect.x, rect.y, rect.width, rect.height / 2); 237 | Rectangle lowerHalf = new Rectangle(rect.x, rect.y + rect.height / 2, rect.width, rect.height / 2); 238 | if (upperHalf.contains(p)) 239 | return i; 240 | if (lowerHalf.contains(p)) 241 | return i + 1; 242 | } 243 | 244 | return entries.size(); 245 | } 246 | 247 | 248 | /** 249 | * Moves an existing entry at the old index to a new index in the list. 250 | * 251 | * @param oldIndex the old index of the entry to move 252 | * @param newIndex the new index for the entry to move 253 | */ 254 | protected void moveIndexToNewIndex(int oldIndex, int newIndex) 255 | { 256 | GameListEntry entryToMove = entries.get(oldIndex); 257 | Game gameToMove = entryToMove.getGame(); 258 | 259 | listPanel.remove(oldIndex); 260 | entries.remove(entryToMove); 261 | 262 | newIndex = oldIndex > newIndex ? newIndex : Math.max(0, newIndex - 1); 263 | 264 | listPanel.add(entryToMove, newIndex); 265 | entries.add(newIndex, entryToMove); 266 | Game.moveGame(gameToMove, newIndex); 267 | 268 | validate(); 269 | repaint(); 270 | } 271 | 272 | 273 | protected int getIndexOfEntry(GameListEntry entry) 274 | { 275 | return entries.indexOf(entry); 276 | } 277 | 278 | 279 | public void addListener(GameListListener listener) 280 | { 281 | listeners.add(listener); 282 | } 283 | 284 | 285 | private void fireEntrySelectedEvent(GameListEntry prevEntry, GameListEntry newEntry) 286 | { 287 | for (GameListListener listener : listeners) 288 | { 289 | listener.entrySelected(prevEntry, newEntry); 290 | } 291 | } 292 | 293 | 294 | private void fireEntryUpdatedEvent(GameListEntry entry) 295 | { 296 | for (GameListListener listener : listeners) 297 | { 298 | listener.entryUpdated(entry); 299 | } 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /src/com/soulsspeedruns/organizer/savelist/SaveListEntry.java: -------------------------------------------------------------------------------- 1 | package com.soulsspeedruns.organizer.savelist; 2 | 3 | 4 | import java.awt.datatransfer.DataFlavor; 5 | import java.awt.datatransfer.Transferable; 6 | import java.awt.datatransfer.UnsupportedFlavorException; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Paths; 11 | import java.nio.file.StandardCopyOption; 12 | import java.util.Collections; 13 | import java.util.LinkedList; 14 | import java.util.List; 15 | import java.util.Objects; 16 | 17 | import javax.swing.JLabel; 18 | import javax.swing.JList; 19 | 20 | import com.soulsspeedruns.organizer.managers.GamesManager; 21 | 22 | 23 | /** 24 | * SaveListEntry class. 25 | *

26 | * Class representing the entries in the Savelist. 27 | * 28 | * @author Kahmul (www.twitch.tv/kahmul78) 29 | * @date 7 Jul 2017 30 | */ 31 | public abstract class SaveListEntry implements Comparable, Transferable 32 | { 33 | 34 | private Folder parent; 35 | private LinkedList children; 36 | private File file; 37 | 38 | private boolean markedForCut; 39 | 40 | public static final DataFlavor ENTRY_FLAVOR = new DataFlavor(SaveListEntry.class, SaveListEntry.class.getSimpleName()); 41 | 42 | 43 | /** 44 | * @param parent 45 | * @param file 46 | */ 47 | public SaveListEntry(Folder parent, File file) 48 | { 49 | this.parent = parent; 50 | this.file = file; 51 | children = new LinkedList<>(); 52 | } 53 | 54 | 55 | /** 56 | * Returns the parent folder of this entry. 57 | * 58 | * @return the parent folder 59 | */ 60 | public Folder getParent() 61 | { 62 | return parent; 63 | } 64 | 65 | 66 | /** 67 | * Returns the file associated with this entry. 68 | * 69 | * @return the file 70 | */ 71 | public File getFile() 72 | { 73 | return file; 74 | } 75 | 76 | 77 | /** 78 | * Replaces the currently associated file with the given one, and updates the associated files for all children. 79 | * 80 | * @param file the new file 81 | */ 82 | public void setFile(File file) 83 | { 84 | String oldPath = getFile().getPath(); 85 | this.file = file; 86 | 87 | // Make sure the files of any children are updated for the new parent path 88 | List children = getChildren(); 89 | for (SaveListEntry child : children) 90 | { 91 | String childPath = child.getFile().getPath(); 92 | childPath = childPath.replace(oldPath, getFile().getPath()); 93 | child.setFile(new File(childPath)); 94 | } 95 | 96 | } 97 | 98 | 99 | /** 100 | * Returns the filename of this entry. 101 | * 102 | * @return the filename 103 | */ 104 | public String getName() 105 | { 106 | return file.getName(); 107 | } 108 | 109 | 110 | /** 111 | * Returns the indent used for the rendering in the SaveList. 112 | * 113 | * @return the indent 114 | */ 115 | protected int getIndent() 116 | { 117 | if (parent.equals(GamesManager.getSelectedProfile().getRoot())) 118 | return 0; 119 | return parent.getIndent() + 20; 120 | } 121 | 122 | 123 | /** 124 | * @return the children 125 | */ 126 | public LinkedList getChildren() 127 | { 128 | return children; 129 | } 130 | 131 | 132 | /** 133 | * @param entry 134 | */ 135 | public void addChild(SaveListEntry entry) 136 | { 137 | children.add(entry); 138 | } 139 | 140 | 141 | /** 142 | * @param entry 143 | */ 144 | public void removeChild(SaveListEntry entry) 145 | { 146 | children.remove(entry); 147 | } 148 | 149 | /** 150 | * Removes all children from this entry. 151 | */ 152 | public void clearChildren() 153 | { 154 | children.clear(); 155 | } 156 | 157 | 158 | /** 159 | * Sorts all children of this entry. 160 | */ 161 | public void sort() 162 | { 163 | Collections.sort(children); 164 | for (SaveListEntry entry : children) 165 | entry.sort(); 166 | } 167 | 168 | 169 | /** 170 | * Moves the entry and all of its children, if any, under the new parent folder. 171 | * 172 | * @param newParent 173 | * @throws IOException 174 | */ 175 | public void moveToNewParent(Folder newParent) throws IOException 176 | { 177 | String parentPath = newParent.getFile().getPath(); 178 | File newFile = new File(parentPath + File.separator + getName()); 179 | 180 | Files.move(Paths.get(getFile().getPath()), Paths.get(newFile.getPath()), StandardCopyOption.REPLACE_EXISTING); 181 | setFile(newFile); 182 | 183 | parent.removeChild(this); 184 | newParent.addChild(this); 185 | parent = newParent; 186 | } 187 | 188 | 189 | /** 190 | * Returns whether this instance or any of its children are a parent of the given entry. 191 | * 192 | * @param entry the entry to check 193 | * @return whether this entry or any of its children are a parent of the given entry 194 | */ 195 | public boolean isParentOf(SaveListEntry entry) 196 | { 197 | if (this.equals(entry.getParent())) 198 | return true; 199 | for (SaveListEntry child : children) 200 | { 201 | if (child.isParentOf(entry)) 202 | return true; 203 | } 204 | return false; 205 | } 206 | 207 | 208 | /** 209 | * Returns the immediate child (no sub-child) of this entry with the given name. 210 | * 211 | * @param name the name of the child 212 | * @return the child with the given name, or null if none is found 213 | */ 214 | public SaveListEntry getChildByName(String name) 215 | { 216 | for (SaveListEntry child : children) 217 | { 218 | if (child.getName().equals(name)) 219 | return child; 220 | } 221 | return null; 222 | } 223 | 224 | 225 | /** 226 | * Returns true if either this entry or one of its children have the given searchTerm in their name. 227 | * 228 | * @param searchTerm the term to search for 229 | * @return whether this entry or one of its children matches the given term 230 | */ 231 | public boolean matchesSearchTerm(String searchTerm) 232 | { 233 | if (getName().toLowerCase().contains(searchTerm.toLowerCase())) 234 | return true; 235 | for (SaveListEntry entry : children) 236 | { 237 | if (entry.matchesSearchTerm(searchTerm)) 238 | return true; 239 | } 240 | return false; 241 | } 242 | 243 | 244 | /** 245 | * Renames the given entry. 246 | * 247 | * @param newName the new name 248 | * @return whether the renaming was successful 249 | */ 250 | public abstract boolean rename(String newName); 251 | 252 | 253 | /** 254 | * Returns whether this entry can currently be renamed. Relevant to check if the file associated with the entry is currently 255 | * being accessed elsewhere. 256 | * 257 | * @return whether the entry can be renamed 258 | */ 259 | public abstract boolean canBeRenamed(); 260 | 261 | 262 | /** 263 | * Returns whether the entry is marked for a cut operation. 264 | * 265 | * @return whether the entry is marked for a cut operation 266 | */ 267 | public boolean isMarkedForCut() 268 | { 269 | return markedForCut; 270 | } 271 | 272 | 273 | /** 274 | * Marks this entry as part of a cut/paste operation. 275 | * 276 | * @param markedForCut whether the entry is marked for a cut operation 277 | */ 278 | public void setMarkedForCut(boolean markedForCut) 279 | { 280 | this.markedForCut = markedForCut; 281 | for (SaveListEntry child : children) { 282 | child.setMarkedForCut(markedForCut); 283 | } 284 | } 285 | 286 | 287 | /** 288 | * Renders this label according to this entry. 289 | * 290 | * @param list the JList of this entry 291 | * @param index the index of this entry in the list 292 | * @param label the label to render 293 | */ 294 | public abstract void render(JList list, int index, JLabel label); 295 | 296 | 297 | /** 298 | * Deletes this entry and the associated file, along with all of its sub-folders and saves, if any. 299 | */ 300 | public abstract void delete(); 301 | 302 | 303 | @Override 304 | public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException 305 | { 306 | if (isDataFlavorSupported(flavor)) 307 | return this; 308 | throw new UnsupportedFlavorException(flavor); 309 | } 310 | 311 | 312 | @Override 313 | public DataFlavor[] getTransferDataFlavors() 314 | { 315 | return new DataFlavor[] { ENTRY_FLAVOR }; 316 | } 317 | 318 | 319 | @Override 320 | public boolean isDataFlavorSupported(DataFlavor flavor) 321 | { 322 | return flavor.equals(ENTRY_FLAVOR); 323 | } 324 | 325 | @Override 326 | public boolean equals(Object o) { 327 | if (this == o) 328 | return true; 329 | if (!(o instanceof SaveListEntry)) 330 | return false; 331 | SaveListEntry entry = (SaveListEntry) o; 332 | return Objects.equals(parent, entry.parent) && Objects.equals(file, entry.file); 333 | } 334 | 335 | } 336 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoulsSpeedruns - Save Organizer 2 | 3 |

4 | Click the banner below to join the SoulsSpeedruns Discord!

5 | 6 |

7 | 8 | ## Table of Contents 9 | 10 | 1. [Description](#description) 11 | 2. [Features](#features) 12 | 3. [Requirements](#requirements) 13 | 4. [Download](#download) 14 | 5. [Getting Started](#getting-started) 15 | 6. [Creating/Loading Savefiles](#creatingloading-savefiles) 16 | 7. [Savefile Locations](#savefile-locations) 17 | 8. [Planned Features](#planned-features) 18 | 9. [Troubleshooting](#troubleshooting) 19 | 10. [Credits](#credits) 20 | 21 | ## Description 22 | 23 | The SoulsSpeedruns - Save Organizer is a tool designed to manage savefiles for Dark Souls, Dark Souls Remastered, Dark Souls II, Dark Souls II: Scholar of the First Sin, Dark Souls III, Sekiro and Elden Ring. Support for more games can be added by the user. 24 | 25 |
26 |

27 | 28 |

29 | 30 | ## Features 31 | 32 | - Manage your savefiles for each supported game, loading or creating them with the click of a button. 33 | - Create profiles for each game to group your savefiles for e.g. speedrun categories. You only see the savefiles assigned to the current profile at any given time. 34 | - Switch the game's savefile to read-only and back with the click of a button for convenient practice without having to manually reload (not supported by DS1R). 35 | - Support for global hotkeys. 36 | - Support out of the box for Dark Souls, Dark Souls Remastered, Dark Souls II, Dark Souls II: Scholar of the First Sin, Dark Souls III, Sekiro and Elden Ring. 37 | - Support for more games can be added by the user (for games that use a single file to store their save data). 38 | - Multiple UI themes, including a SoulsSpeedruns theme in the style of the [SoulsSpeedruns wiki](https://soulsspeedruns.com/) dark mode. 39 | 40 | ⚠️ **This application is primarily intended for speedrunners or challenge runners. There is little to no safeguarding for keeping your savefiles if you modify or delete them. If you wish to use this program for backing up your first playthrough or similar, make sure you know what you are doing and ideally make a separate backup of your savefiles elsewhere in case you mess up!** ⚠️ 41 | 42 | ## Requirements 43 | 44 | - Java JRE 8 or higher (included in the bundled version) 45 | 46 | ## Download 47 | 48 | [Latest Release](https://github.com/Kahmul/SpeedSouls-Save-Organizer/releases) 49 | 50 | - **Windows version:** Runnable .exe file. Requires Java JRE 8+ to be installed on your system. 51 | - **Bundled Windows version:** Runnable .exe file. Bundled with JRE 8. 52 | - **Linux version:** Runnable .jar file. Run it via CLI java -jar "SoulsSpeedruns - Save Organizer.jar" or similar. 53 | 54 | ## Getting Started 55 | 56 | To get started using the save organizer, follow these steps: 57 | 58 | 1. Start the application, press **Edit Games** in the top right. 59 | 2. In the **Games Configuration** window choose the game you wish to create savefiles for on the side. 60 | 3. Click **Browse** under 'Savefile Location' to choose the file the game uses to store its savedata. If you are choosing the savefile for a Souls game, the organizer will try to automatically find the savefile in its usual default location, so most users will just need to confirm the suggested selection. If you already tinkered with savefiles manually, make sure that you select the correct savefile the game uses. This can usually be verified by checking the date it was last modified. See also [Savefile Locations](#savefile-locations). 61 | 4. The application will ask you if you wish to store your savefiles in the same directory where the game's savefile is stored. You can either agree or choose a different one under 'Profiles Directory'. If you already have existing profiles on your PC, you should point the organizer to that directory in this step. 62 | 5. Press **New** to create a new profile for your game. Name it whatever you'd like, e.g. the name of the category you wish to run. 63 | 6. Once you are done creating your profiles you can close the **Games Configuration** window. 64 | 7. Back in the main window you can now choose the game and your profile(s) at the top. 65 | 8. Start creating savefiles by pressing **Import Savestate**, or **Rightclick > Add Folder** to create folders within your profiles. Savefiles will be imported into the selected folder. 66 | 9. Load your created savefiles by pressing **Load Savestate**. Check the next section [Creating/Loading Savefiles](#creatingloading-savefiles) for more information. 67 | 10. *(Optional)* Click the cog button in the bottom right to access settings. You can enable global hotkeys there to conveniently work with the organizer during practice, as well as change the UI theme of the organizer. 68 | 11. *(Optional)* If you wish to add support for other games besides the Souls games, click the + in the top left of the **Games Configuration** window. There you can create a custom game by giving it a name and telling the organizer what file name to expect for the savefile. Once created, the game functions like any other game in the organizer. You can adjust the order of the games via drag and drop if you wish to have your custom games higher up the list. 69 | 70 | ## Creating/Loading Savefiles 71 | 72 | Due to the how the different FROMSoftware games work, there is a few things to consider when creating and loading savefiles. 73 | 74 | - All characters are stored in the single savefile the game uses. 75 | - Generally you want to quit out to the main menu before creating savefiles. This is not strictly necessary but it makes sure all progress has been saved to the savefile beforehand. Users more familiar with the games may want to force a save by e.g. simply opening/closing the start menu. You can then create a savefile during gameplay, or switch to read-only to repeat the upcoming section multiple times without having to quitout an extra time beforehand. 76 | - Loading savefiles only works in the main menu. Loading a savefile in the middle of gameplay will do nothing as the game will simply overwrite it again the next time it saves. 77 | - When loading a savefile in the main menu and looking at your characters, you may notice that nothing has changed. This is normal, the game only updates that info when the main menu is reloaded. If you choose a character slot, the corresponding character from the loaded savefile will be nonetheless correctly loaded. 78 | 79 | ## Savefile Locations 80 | 81 | The organizer should automatically find your savefile when you open the respective file browser as outlined in [Getting Started](#getting-started), so you should not need to search for it. 82 | If you tinkered with your savefiles manually in advance however, or are otherwise curious where the savefiles can be found, here are their default locations on Windows: 83 | 84 | - Dark Souls: C:\Users\\\Documents\NBGI\DarkSouls\DRAKS0005.sl2 85 | - Dark Souls Remastered: C:\Users\\\Documents\NBGI\DARK SOULS REMASTERED\DRAKS0005.sl2 86 | - Dark Souls II: C:\Users\\\AppData\Roaming\DarkSoulsII\\\DARKSII0000.sl2 87 | - Dark Souls II: SotFS: C:\Users\\\AppData\Roaming\DarkSoulsII\\\DS2SOFS0000.sl2 88 | - Dark Souls III: C:\Users\\\AppData\Roaming\DarkSoulsIII\\\DS30000.sl2 89 | - Sekiro: C:\Users\\\AppData\Roaming\Sekiro\\\S0000.sl2 90 | - Elden Ring: C:\Users\\\AppData\Roaming\EldenRing\\\ER0000.sl2 91 | 92 | ## Planned Features 93 | 94 | - Editing savefiles to adjust stats/items for characters (likely only for DS1) 95 | 96 | ## Troubleshooting 97 | 98 | - You can join the [SoulsSpeedruns Discord](https://discord.soulsspeedruns.com) for troubleshooting help. 99 | - Make sure you use Java JRE 8. You can use the bundled version for this. 100 | - If you're on Steam Deck and there is no visible UI on the organizer, you may need to set the _JAVA_AWT_WM_NONREPARENTING=1 environment variable. 101 | - If you have problems starting up the program after you've already been using it there might be conflicting data between the registry entries and the actual data. In this case it might help to remove the registry entries of the Save Organizer (the following steps are for Windows): 102 | 103 | 1. Press Windows + R. 104 | 2. Enter "regedit". 105 | 3. Navigate to "HKEY_CURRENT_USER\Software\JavaSoft\Prefs\com\soulsspeedruns\organizer\prefs". 106 | 4. Delete all entries. 107 | 108 | ## Credits 109 | 110 | - johndisandonato for adding the 'Highlight Previous/Next Savestate' hotkeys 111 | --------------------------------------------------------------------------------