Skip to content

Commit

Permalink
CustomDataDirectory: introduce new setting
Browse files Browse the repository at this point in the history
For some users, it might be useful to change the data dir location.
This is especially useful on platforms which don't support XDG_DATA_HOME
environment variable.

Closes: #2
  • Loading branch information
rybak committed Jul 9, 2022
1 parent d7b4b59 commit d3b35f4
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 17 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -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
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/dev/andrybak/resoday/Resoday.java
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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);
Expand All @@ -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<Path> maybeCustomDataDir = CustomDataDirectory.from(configDir);
if (maybeCustomDataDir.isPresent()) {
dataDir = maybeCustomDataDir.get();
System.out.println("Using custom data directory: " + dataDir);
} else {
dataDir = defaultDataDir;
}
return dataDir;
}
}
5 changes: 5 additions & 0 deletions src/main/java/dev/andrybak/resoday/YearHistory.java
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ public IntStream years() {
.distinct();
}

public void forceSave() {
hasChanges = true;
save();
}

public void save() {
if (!hasChanges) {
return;
Expand Down
27 changes: 20 additions & 7 deletions src/main/java/dev/andrybak/resoday/gui/MainGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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() {
Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Path> maybePath = show(jFrame);
System.out.println(maybePath.map(p -> "Got new path: " + p).orElse("Got no path."));
}

public static Optional<Path> 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();
}
}
}
122 changes: 121 additions & 1 deletion src/main/java/dev/andrybak/resoday/gui/settings/SettingsMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,29 @@
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 {
private SettingsMenu() {
throw new UnsupportedOperationException();
}

public static JMenu create(GuiSettings current, Consumer<GuiSettings> newSettingsConsumer) {
public static JMenu create(GuiSettings current, Consumer<GuiSettings> newSettingsConsumer,
DataDirSupplier dataDirSupplier, Consumer<Path> newDataDirConsumer)
{
JMenu menu = new JMenu("Settings");
menu.setMnemonic('S');
{
Expand All @@ -29,6 +42,113 @@ public static JMenu create(GuiSettings current, Consumer<GuiSettings> 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<Path> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Path> from(Path configDir) {
Path maybeFile = configDir.resolve(CUSTOM_DATA_DIR_FILE);
if (Files.isRegularFile(maybeFile) && Files.isReadable(maybeFile)) {
try {
List<String> 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);
}
}
}
18 changes: 11 additions & 7 deletions src/main/java/dev/andrybak/resoday/storage/SortOrder.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,7 @@ public class SortOrder {
}

public static void save(DataDirSupplier dataDirSupplier, List<String> 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<SortOrder> read(Path rootDir) {
Expand All @@ -52,6 +46,16 @@ public static Optional<SortOrder> 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 <T> Stream<T> order(Map<String, T> elements) {
Set<String> inputIds = new HashSet<>(elements.keySet());
Set<String> actualOrder = new LinkedHashSet<>(); // LinkedHashSet because we need preserved order
Expand Down

0 comments on commit d3b35f4

Please sign in to comment.