diff --git a/CHANGELOG b/CHANGELOG index 0aa62f4..a20a232 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +- Custom data directory setting has been introduced + v1.2 - Java 17 is now required to run Resoday - Menu "Help > Help" has been introduced diff --git a/src/main/java/dev/andrybak/resoday/Resoday.java b/src/main/java/dev/andrybak/resoday/Resoday.java index fd28c78..50c3a2a 100644 --- a/src/main/java/dev/andrybak/resoday/Resoday.java +++ b/src/main/java/dev/andrybak/resoday/Resoday.java @@ -1,12 +1,14 @@ package dev.andrybak.resoday; import dev.andrybak.resoday.gui.MainGui; +import dev.andrybak.resoday.settings.storage.CustomDataDirectory; import dev.dirs.ProjectDirectories; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Optional; public final class Resoday { @@ -16,12 +18,12 @@ public static void main(String... args) { if (args.length < 1) { ProjectDirectories projectDirs = ProjectDirectories.from(StringConstants.TOP_LEVEL, StringConstants.ORGANIZATION, StringConstants.APP_NAME); - dataDir = Paths.get(projectDirs.dataDir).toAbsolutePath(); configDir = Paths.get(projectDirs.configDir).toAbsolutePath(); + dataDir = getDataDir(configDir, Paths.get(projectDirs.dataDir).toAbsolutePath()); } else { Path p = Paths.get(args[0]).toAbsolutePath(); - dataDir = p; configDir = p; + dataDir = getDataDir(configDir, p); } try { Files.createDirectories(dataDir); @@ -44,4 +46,16 @@ public static void main(String... args) { } new MainGui(dataDir, configDir).go(configDir); } + + private static Path getDataDir(Path configDir, Path defaultDataDir) { + final Path dataDir; + final Optional maybeCustomDataDir = CustomDataDirectory.from(configDir); + if (maybeCustomDataDir.isPresent()) { + dataDir = maybeCustomDataDir.get(); + System.out.println("Using custom data directory: " + dataDir); + } else { + dataDir = defaultDataDir; + } + return dataDir; + } } diff --git a/src/main/java/dev/andrybak/resoday/YearHistory.java b/src/main/java/dev/andrybak/resoday/YearHistory.java index fff1150..ace1d8b 100644 --- a/src/main/java/dev/andrybak/resoday/YearHistory.java +++ b/src/main/java/dev/andrybak/resoday/YearHistory.java @@ -191,6 +191,11 @@ public IntStream years() { .distinct(); } + public void forceSave() { + hasChanges = true; + save(); + } + public void save() { if (!hasChanges) { return; diff --git a/src/main/java/dev/andrybak/resoday/gui/MainGui.java b/src/main/java/dev/andrybak/resoday/gui/MainGui.java index f2dd914..1b20af6 100644 --- a/src/main/java/dev/andrybak/resoday/gui/MainGui.java +++ b/src/main/java/dev/andrybak/resoday/gui/MainGui.java @@ -14,6 +14,7 @@ import dev.andrybak.resoday.gui.settings.SettingsMenu; import dev.andrybak.resoday.settings.gui.CalendarLayoutSetting; import dev.andrybak.resoday.settings.gui.GuiSettings; +import dev.andrybak.resoday.settings.storage.CustomDataDirectory; import dev.andrybak.resoday.storage.HabitFiles; import dev.andrybak.resoday.storage.SortOrder; @@ -67,7 +68,7 @@ public final class MainGui implements CalendarLayoutSettingProvider { private final Histories histories = new Histories(); private final Timer autoSaveTimer; private GuiSettings guiSettings; - private final Path dataDir; + private Path dataDir; private final GuiSettingsSaver guiSettingsSaver = new GuiSettingsSaver(); public MainGui(Path dataDir, Path configDir) { @@ -119,7 +120,7 @@ public MainGui(Path dataDir, Path configDir) { autoSaveTimer = new Timer(Math.toIntExact(AUTO_SAVE_PERIOD.toMillis()), ignored -> autoSave(configDir)); autoSaveTimer.addActionListener(ignored -> histories.forEachPanel(HistoryPanel::updateDecorations)); - setUpMenuBar(tabs); + setUpMenuBar(tabs, configDir); } private static Image getResodayImage() { @@ -135,7 +136,7 @@ private void markTodayInCurrentTab(JTabbedPane tabs) { getCurrentHistoryPanel(tabs).ifPresent(HistoryPanel::markToday); } - private void setUpMenuBar(JTabbedPane tabs) { + private void setUpMenuBar(JTabbedPane tabs, Path configDir) { JMenuBar menuBar = new JMenuBar(); JMenu mainMenu = new JMenu("Main"); @@ -173,10 +174,22 @@ private void setUpMenuBar(JTabbedPane tabs) { } menuBar.add(mainMenu); - JMenu settingsMenu = SettingsMenu.create(guiSettings, newSettings -> { - guiSettings = newSettings; - histories.forEachPanel(p -> p.newSettings(this)); - }); + JMenu settingsMenu = SettingsMenu.create( + guiSettings, newSettings -> { + guiSettings = newSettings; + histories.forEachPanel(p -> p.newSettings(this)); + }, + getDataDirSupplier(), newDataDir -> { + System.out.println("Trying to move data to directory '" + newDataDir + "'"); + Path oldDataDir = dataDir; + dataDir = newDataDir; + // Re-save everything in package `dev.andrybak.resoday.storage` into new data dir. + // Hopefully in the future no new kinds of files will be saved in the data dir :-) + SortOrder.read(oldDataDir).ifPresent(order -> order.save(getDataDirSupplier())); + histories.forEachHistory(YearHistory::forceSave); + CustomDataDirectory.save(configDir, dataDir); + } + ); menuBar.add(settingsMenu); JMenu helpMenu = new JMenu("Help"); diff --git a/src/main/java/dev/andrybak/resoday/gui/settings/CustomDataDirectoryDialog.java b/src/main/java/dev/andrybak/resoday/gui/settings/CustomDataDirectoryDialog.java new file mode 100644 index 0000000..55dc8c6 --- /dev/null +++ b/src/main/java/dev/andrybak/resoday/gui/settings/CustomDataDirectoryDialog.java @@ -0,0 +1,33 @@ +package dev.andrybak.resoday.gui.settings; + +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.WindowConstants; +import java.awt.Component; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + +public class CustomDataDirectoryDialog { + public static void main(String[] args) { + JFrame jFrame = new JFrame("Dir chooser demo"); + jFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + jFrame.setVisible(true); + Optional maybePath = show(jFrame); + System.out.println(maybePath.map(p -> "Got new path: " + p).orElse("Got no path.")); + } + + public static Optional show(Component owner) { + JFileChooser dirChooser = new JFileChooser(Paths.get(".").toFile()); + dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int selectedOption = dirChooser.showDialog(owner, "Select"); + if (selectedOption == JFileChooser.APPROVE_OPTION) { + File d = dirChooser.getSelectedFile(); + System.out.println("Chosen: " + d); + return Optional.of(d.toPath()); + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/dev/andrybak/resoday/gui/settings/SettingsMenu.java b/src/main/java/dev/andrybak/resoday/gui/settings/SettingsMenu.java index cfa91bd..f279769 100644 --- a/src/main/java/dev/andrybak/resoday/gui/settings/SettingsMenu.java +++ b/src/main/java/dev/andrybak/resoday/gui/settings/SettingsMenu.java @@ -4,8 +4,19 @@ import dev.andrybak.resoday.settings.gui.GuiSettings; import javax.swing.ButtonGroup; +import javax.swing.JLabel; import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; import javax.swing.JRadioButtonMenuItem; +import javax.swing.JTextField; +import java.awt.BorderLayout; +import java.awt.Desktop; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; import java.util.function.Consumer; public final class SettingsMenu { @@ -13,7 +24,9 @@ private SettingsMenu() { throw new UnsupportedOperationException(); } - public static JMenu create(GuiSettings current, Consumer newSettingsConsumer) { + public static JMenu create(GuiSettings current, Consumer newSettingsConsumer, + DataDirSupplier dataDirSupplier, Consumer newDataDirConsumer) + { JMenu menu = new JMenu("Settings"); menu.setMnemonic('S'); { @@ -29,6 +42,113 @@ public static JMenu create(GuiSettings current, Consumer newSetting } menu.add(calendarLayoutMenu); } + { + JMenu dataDirMenu = new JMenu("Data directory"); + dataDirMenu.setMnemonic('D'); + { + JMenuItem openDataDir = new JMenuItem("Open data directory"); + openDataDir.setMnemonic('O'); + openDataDir.addActionListener(ignored -> { + JOptionPane.showMessageDialog( + JOptionPane.getFrameForComponent(menu), + "Edit the files only if you know what you're doing.", + "Be careful", + JOptionPane.WARNING_MESSAGE + ); + Path dataDir = dataDirSupplier.getDataDir(); + System.out.println("Opening '" + dataDir.toAbsolutePath() + "'..."); + Desktop desktop = Desktop.getDesktop(); + if (desktop.isSupported(Desktop.Action.OPEN)) { + try { + desktop.open(dataDir.toFile()); + } catch (IOException e) { + showDataDirError(menu, dataDir); + } + } else { + showDataDirError(menu, dataDir); + } + }); + dataDirMenu.add(openDataDir); + } + { + JMenuItem customDataDirMenuItem = new JMenuItem("Custom data directory"); + customDataDirMenuItem.setMnemonic('U'); + customDataDirMenuItem.addActionListener(ignored -> { + final int confirmedBefore = JOptionPane.showConfirmDialog( + JOptionPane.getFrameForComponent(menu), + "Setting custom data directory is an advanced action." + + " Make sure you know what you are doing with the directory." + + " Are you sure you want to continue?", + "Advanced setting", + JOptionPane.YES_NO_OPTION + ); + if (confirmedBefore != JOptionPane.YES_OPTION) { + System.out.println("Aborted choosing custom directory after first warning."); + return; + } + + Optional maybePath = CustomDataDirectoryDialog.show(JOptionPane.getFrameForComponent(menu)); + + if (maybePath.isEmpty()) { + System.out.println("Aborted choosing custom directory after file chooser."); + return; + } + final Path newDataDir = maybePath.get(); + if (dataDirSupplier.getDataDir().equals(newDataDir)) { + System.out.println("Same directory was chosen: " + newDataDir.toAbsolutePath()); + System.out.println("Aborted changing data directory."); + return; + } + if (!Files.exists(newDataDir) || !Files.isDirectory(newDataDir)) { + JOptionPane.showMessageDialog( + JOptionPane.getFrameForComponent(menu), + "Could not find directory '" + newDataDir + "'.", + "Error", + JOptionPane.ERROR_MESSAGE + ); + return; + } + final int confirmedAfter = JOptionPane.showConfirmDialog( + JOptionPane.getFrameForComponent(menu), + getSecondDataDirWarningMessage(newDataDir), + "Set custom data directory?", + JOptionPane.YES_NO_OPTION + ); + if (confirmedAfter != JOptionPane.YES_OPTION) { + System.out.println("Aborted choosing custom directory after second warning."); + return; + } + newDataDirConsumer.accept(newDataDir); + }); + dataDirMenu.add(customDataDirMenuItem); + } + menu.add(dataDirMenu); + } return menu; } + + private static void showDataDirError(JMenu menu, Path dataDir) { + JPanel message = new JPanel(new BorderLayout()); + { + message.add(new JLabel("Could not open the data directory automatically."), BorderLayout.NORTH); + message.add(new JLabel("You can copy the path from the field below."), BorderLayout.CENTER); + JTextField textField = new JTextField(dataDir.toAbsolutePath().toString()); + textField.setEditable(false); + message.add(textField, BorderLayout.SOUTH); + } + JOptionPane.showMessageDialog( + JOptionPane.getFrameForComponent(menu), + message, + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + + private static Object getSecondDataDirWarningMessage(Path newDataDir) { + JPanel message = new JPanel(new BorderLayout()); + message.add(new JLabel("Are you sure you want to use '" + newDataDir.toAbsolutePath() + "' as custom data dir?"), + BorderLayout.CENTER); + message.add(new JLabel("All files in the directory will be overwritten."), BorderLayout.SOUTH); + return message; + } } diff --git a/src/main/java/dev/andrybak/resoday/settings/storage/CustomDataDirectory.java b/src/main/java/dev/andrybak/resoday/settings/storage/CustomDataDirectory.java new file mode 100644 index 0000000..d377f80 --- /dev/null +++ b/src/main/java/dev/andrybak/resoday/settings/storage/CustomDataDirectory.java @@ -0,0 +1,43 @@ +package dev.andrybak.resoday.settings.storage; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; + +public class CustomDataDirectory { + private static final Path CUSTOM_DATA_DIR_FILE = Paths.get("custom-data-directory.txt"); + + private CustomDataDirectory() { + throw new AssertionError(); + } + + public static Optional from(Path configDir) { + Path maybeFile = configDir.resolve(CUSTOM_DATA_DIR_FILE); + if (Files.isRegularFile(maybeFile) && Files.isReadable(maybeFile)) { + try { + List strings = Files.readAllLines(maybeFile); + if (strings.isEmpty()) { + return Optional.empty(); + } + Path customDataDir = Path.of(strings.get(0)); + return Optional.of(customDataDir); + } catch (IOException e) { + throw new UncheckedIOException("Could not read " + maybeFile, e); + } + } + return Optional.empty(); + } + + public static void save(Path configDir, Path customDataDir) { + Path f = configDir.resolve(CUSTOM_DATA_DIR_FILE); + try { + Files.writeString(f, customDataDir.toAbsolutePath().toString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/src/main/java/dev/andrybak/resoday/storage/SortOrder.java b/src/main/java/dev/andrybak/resoday/storage/SortOrder.java index 192b06b..117e12b 100644 --- a/src/main/java/dev/andrybak/resoday/storage/SortOrder.java +++ b/src/main/java/dev/andrybak/resoday/storage/SortOrder.java @@ -26,13 +26,7 @@ public class SortOrder { } public static void save(DataDirSupplier dataDirSupplier, List ids) { - Path p = dataDirSupplier.getDataDir().resolve(ORDER_FILE); - try { - Files.write(p, ids); - } catch (IOException e) { - System.err.println("Could not write '" + p + "'. Got error: " + e); - e.printStackTrace(); - } + new SortOrder(ids).save(dataDirSupplier); } public static Optional read(Path rootDir) { @@ -52,6 +46,16 @@ public static Optional read(Path rootDir) { } } + public void save(DataDirSupplier dataDirSupplier) { + Path p = dataDirSupplier.getDataDir().resolve(ORDER_FILE); + try { + Files.write(p, order); + } catch (IOException e) { + System.err.println("Could not write '" + p + "'. Got error: " + e); + e.printStackTrace(); + } + } + public Stream order(Map elements) { Set inputIds = new HashSet<>(elements.keySet()); Set actualOrder = new LinkedHashSet<>(); // LinkedHashSet because we need preserved order