diff --git a/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelExportDialog.java b/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelExportDialog.java index 55262f128..8cbfc0dc5 100644 --- a/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelExportDialog.java +++ b/modules/decima-ext-model-exporter/src/main/java/com/shade/decima/ui/data/viewer/model/ModelExportDialog.java @@ -8,6 +8,7 @@ import com.shade.platform.model.runtime.ProgressMonitor; import com.shade.platform.model.util.IOUtils; import com.shade.platform.ui.controls.ColoredListCellRenderer; +import com.shade.platform.ui.controls.FileChooser; import com.shade.platform.ui.controls.TextAttributes; import com.shade.platform.ui.dialogs.BaseDialog; import com.shade.platform.ui.dialogs.ProgressDialog; @@ -112,7 +113,7 @@ protected void buttonPressed(@NotNull ButtonDescriptor descriptor) { final ModelExporterProvider provider = exporterCombo.getItemAt(exporterCombo.getSelectedIndex()); final String extension = provider.getExtension(); - final JFileChooser chooser = new JFileChooser(); + final JFileChooser chooser = new FileChooser(); chooser.setDialogTitle("Save model as"); chooser.setFileFilter(new FileExtensionFilter("%s Files".formatted(provider.getName()), extension)); chooser.setSelectedFile(new File("%s.%s".formatted("exported", extension))); diff --git a/modules/decima-ext-texture-viewer/src/main/java/com/shade/decima/ui/data/viewer/texture/TextureExportDialog.java b/modules/decima-ext-texture-viewer/src/main/java/com/shade/decima/ui/data/viewer/texture/TextureExportDialog.java index 82fcd353c..741eb34e3 100644 --- a/modules/decima-ext-texture-viewer/src/main/java/com/shade/decima/ui/data/viewer/texture/TextureExportDialog.java +++ b/modules/decima-ext-texture-viewer/src/main/java/com/shade/decima/ui/data/viewer/texture/TextureExportDialog.java @@ -5,6 +5,7 @@ import com.shade.decima.ui.data.viewer.texture.controls.ImageProvider; import com.shade.platform.model.data.DataKey; import com.shade.platform.ui.controls.ColoredListCellRenderer; +import com.shade.platform.ui.controls.FileChooser; import com.shade.platform.ui.controls.TextAttributes; import com.shade.platform.ui.dialogs.BaseDialog; import com.shade.platform.ui.dialogs.ProgressDialog; @@ -91,7 +92,7 @@ protected void buttonPressed(@NotNull ButtonDescriptor descriptor) { final String name = Objects.requireNonNullElse(provider.getName(), "exported"); final String extension = exporter.getExtension(); - final JFileChooser chooser = new JFileChooser(); + final JFileChooser chooser = new FileChooser(); chooser.setDialogTitle("Save texture as"); chooser.setFileFilter(new FileExtensionFilter("%s Files".formatted(extension.toUpperCase()), extension)); chooser.setSelectedFile(new File("%s.%s".formatted(name, extension))); diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/audio/menu/ExportAllTracksItem.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/audio/menu/ExportAllTracksItem.java index 34f3b32b7..987fb6348 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/audio/menu/ExportAllTracksItem.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/audio/menu/ExportAllTracksItem.java @@ -6,6 +6,7 @@ import com.shade.decima.ui.data.viewer.audio.AudioPlayerPanel; import com.shade.decima.ui.data.viewer.audio.AudioPlayerUtils; import com.shade.decima.ui.data.viewer.audio.Playlist; +import com.shade.platform.ui.controls.FileChooser; import com.shade.platform.ui.dialogs.ProgressDialog; import com.shade.platform.ui.menus.MenuItem; import com.shade.platform.ui.menus.MenuItemContext; @@ -27,7 +28,7 @@ public class ExportAllTracksItem extends MenuItem { @Override public void perform(@NotNull MenuItemContext ctx) { - final JFileChooser chooser = new JFileChooser(); + final JFileChooser chooser = new FileChooser(); chooser.setDialogTitle("Save tracks to"); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.addChoosableFileFilter(new FileExtensionFilter("OGG Files", "ogg")); diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/binary/BinaryViewerPanel.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/binary/BinaryViewerPanel.java index 74c1c559d..5610a18c5 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/binary/BinaryViewerPanel.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/binary/BinaryViewerPanel.java @@ -9,6 +9,7 @@ import com.shade.platform.model.util.BufferUtils; import com.shade.platform.model.util.IOUtils; import com.shade.platform.ui.UIColor; +import com.shade.platform.ui.controls.FileChooser; import com.shade.platform.ui.util.UIUtils; import com.shade.util.NotNull; import com.shade.util.Nullable; @@ -132,7 +133,7 @@ public ExportAction() { @Override public void actionPerformed(ActionEvent event) { - final JFileChooser chooser = new JFileChooser(); + final JFileChooser chooser = new FileChooser(); chooser.setDialogTitle("Export binary data as"); chooser.setSelectedFile(new File("exported.bin")); chooser.setAcceptAllFileFilterUsed(true); diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/font/FontExportDialog.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/font/FontExportDialog.java index b85c1e16d..029474ee8 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/font/FontExportDialog.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/data/viewer/font/FontExportDialog.java @@ -3,6 +3,7 @@ import com.shade.decima.model.rtti.types.java.HwFont; import com.shade.decima.ui.controls.FileExtensionFilter; import com.shade.platform.ui.controls.ColoredListCellRenderer; +import com.shade.platform.ui.controls.FileChooser; import com.shade.platform.ui.controls.TextAttributes; import com.shade.platform.ui.dialogs.BaseDialog; import com.shade.util.NotNull; @@ -59,7 +60,7 @@ protected void buttonPressed(@NotNull ButtonDescriptor descriptor) { final String name = Objects.requireNonNullElse(font.getName(), "font"); final String extension = exporter.getExtension(); - final JFileChooser chooser = new JFileChooser(); + final JFileChooser chooser = new FileChooser(); chooser.setDialogTitle("Save font as"); chooser.setFileFilter(new FileExtensionFilter("%s Files".formatted(extension.toUpperCase()), extension)); chooser.setSelectedFile(new File("%s.%s".formatted(name, extension))); diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/dialogs/PersistChangesDialog.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/dialogs/PersistChangesDialog.java index 0e133d38d..b517735ad 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/dialogs/PersistChangesDialog.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/dialogs/PersistChangesDialog.java @@ -19,10 +19,7 @@ import com.shade.decima.ui.navigator.impl.NavigatorProjectNode; import com.shade.platform.model.runtime.ProgressMonitor; import com.shade.platform.model.util.IOUtils; -import com.shade.platform.ui.controls.ColoredListCellRenderer; -import com.shade.platform.ui.controls.CommonTextAttributes; -import com.shade.platform.ui.controls.Mnemonic; -import com.shade.platform.ui.controls.TextAttributes; +import com.shade.platform.ui.controls.*; import com.shade.platform.ui.dialogs.BaseDialog; import com.shade.platform.ui.dialogs.ProgressDialog; import com.shade.platform.ui.util.UIUtils; @@ -221,7 +218,7 @@ protected void buttonPressed(@NotNull ButtonDescriptor descriptor) { outputPath = null; } else { - final JFileChooser chooser = new JFileChooser(); + final JFileChooser chooser = new FileChooser(); chooser.setDialogTitle("Choose output packfile"); chooser.setFileFilter(new FileExtensionFilter("Decima packfile", "bin")); chooser.setAcceptAllFileFilterUsed(false); diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/menu/ExportToJsonItem.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/menu/ExportToJsonItem.java index 204d3b30a..803951e82 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/menu/ExportToJsonItem.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/editor/core/menu/ExportToJsonItem.java @@ -11,6 +11,7 @@ import com.shade.decima.ui.editor.core.CoreNodeFile; import com.shade.decima.ui.editor.core.CoreNodeObject; import com.shade.platform.ui.PlatformDataKeys; +import com.shade.platform.ui.controls.FileChooser; import com.shade.platform.ui.menus.MenuItem; import com.shade.platform.ui.menus.MenuItemContext; import com.shade.platform.ui.menus.MenuItemRegistration; @@ -28,7 +29,7 @@ public class ExportToJsonItem extends MenuItem { @Override public void perform(@NotNull MenuItemContext ctx) { - final JFileChooser chooser = new JFileChooser(); + final JFileChooser chooser = new FileChooser(); chooser.setDialogTitle("Save as"); chooser.setFileFilter(new FileExtensionFilter("JSON Files", "json")); chooser.setSelectedFile(new File("exported.json")); diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ExportContentsItem.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ExportContentsItem.java index 4f8c1b532..5ab4ec7e0 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ExportContentsItem.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ExportContentsItem.java @@ -1,7 +1,9 @@ package com.shade.decima.ui.navigator.menu; +import com.shade.decima.model.util.FilePath; import com.shade.decima.ui.navigator.impl.NavigatorFileNode; import com.shade.platform.ui.PlatformDataKeys; +import com.shade.platform.ui.controls.FileChooser; import com.shade.platform.ui.menus.MenuItem; import com.shade.platform.ui.menus.MenuItemContext; import com.shade.platform.ui.menus.MenuItemRegistration; @@ -10,27 +12,48 @@ import javax.swing.*; import java.io.*; import java.nio.file.Files; +import java.nio.file.Path; import static com.shade.decima.ui.menu.MenuConstants.*; import static java.nio.file.StandardOpenOption.*; -@MenuItemRegistration(parent = CTX_MENU_NAVIGATOR_ID, name = "E&xport File\u2026", icon = "Action.exportIcon", group = CTX_MENU_NAVIGATOR_GROUP_EDIT, order = 2000) +@MenuItemRegistration(parent = CTX_MENU_NAVIGATOR_ID, name = "E&xport File\u2026", icon = "Action.exportIcon", group = CTX_MENU_NAVIGATOR_GROUP_EDIT, order = 1000) public class ExportContentsItem extends MenuItem { @Override public void perform(@NotNull MenuItemContext ctx) { final NavigatorFileNode node = (NavigatorFileNode) ctx.getData(PlatformDataKeys.SELECTION_KEY); - final JFileChooser chooser = new JFileChooser(); - chooser.setSelectedFile(new File(node.getLabel())); + final FilePath path = node.getPath(); + + final JCheckBox keepDirectoryStructure = new JCheckBox("Keep directory structure"); + keepDirectoryStructure.setToolTipText("Creates subdirectories for the exported file"); + + final FileChooser chooser = new FileChooser(); + chooser.setSelectedFile(new File(path.last())); + chooser.setOptions(keepDirectoryStructure); + + if (chooser.showSaveDialog(JOptionPane.getRootFrame()) != JFileChooser.APPROVE_OPTION) { + return; + } + + try { + final Path file = chooser.getSelectedFile().toPath(); + final Path output; + + if (keepDirectoryStructure.isSelected() && path.length() > 1) { + output = file.resolveSibling(path.full()); + Files.createDirectories(output.getParent()); + } else { + output = file; + } - if (chooser.showSaveDialog(JOptionPane.getRootFrame()) == JFileChooser.APPROVE_OPTION) { try ( InputStream is = node.getFile().newInputStream(); - OutputStream os = Files.newOutputStream(chooser.getSelectedFile().toPath(), CREATE, WRITE, TRUNCATE_EXISTING) + OutputStream os = Files.newOutputStream(output, CREATE, WRITE, TRUNCATE_EXISTING) ) { is.transferTo(os); - } catch (IOException e) { - throw new UncheckedIOException(e); } + } catch (IOException e) { + throw new UncheckedIOException(e); } } diff --git a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ImportContentsItem.java b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ImportContentsItem.java index af9ca9804..8bbf89744 100644 --- a/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ImportContentsItem.java +++ b/modules/decima-ui/src/main/java/com/shade/decima/ui/navigator/menu/ImportContentsItem.java @@ -14,7 +14,7 @@ import static com.shade.decima.ui.menu.MenuConstants.*; -@MenuItemRegistration(parent = CTX_MENU_NAVIGATOR_ID, name = "I&mport File\u2026", icon = "Action.importIcon", group = CTX_MENU_NAVIGATOR_GROUP_EDIT, order = 1000) +@MenuItemRegistration(parent = CTX_MENU_NAVIGATOR_ID, name = "I&mport File\u2026", icon = "Action.importIcon", group = CTX_MENU_NAVIGATOR_GROUP_EDIT, order = 2000) public class ImportContentsItem extends MenuItem { @Override public void perform(@NotNull MenuItemContext ctx) { diff --git a/modules/platform-ui/src/main/java/com/shade/platform/ui/controls/FileChooser.java b/modules/platform-ui/src/main/java/com/shade/platform/ui/controls/FileChooser.java new file mode 100644 index 000000000..adb746f97 --- /dev/null +++ b/modules/platform-ui/src/main/java/com/shade/platform/ui/controls/FileChooser.java @@ -0,0 +1,44 @@ +package com.shade.platform.ui.controls; + +import com.shade.util.Nullable; + +import javax.swing.*; +import java.io.File; + +public class FileChooser extends JFileChooser { + public static final String OPTIONS_CHANGED_PROPERTY = "optionsChanged"; + + private JComponent options; + + @Override + public void approveSelection() { + final File file = getSelectedFile(); + + if (file != null && file.exists() && getDialogType() == SAVE_DIALOG) { + final int result = JOptionPane.showConfirmDialog( + this, + "File '%s' already exists. Do you want to overwrite it?".formatted(file.getName()), + "Confirm", + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE + ); + + if (result != JOptionPane.OK_OPTION) { + return; + } + } + + super.approveSelection(); + } + + @Nullable + public JComponent getOptions() { + return options; + } + + public void setOptions(@Nullable JComponent options) { + final JComponent oldValue = this.options; + this.options = options; + firePropertyChange(OPTIONS_CHANGED_PROPERTY, oldValue, options); + } +} diff --git a/modules/platform-ui/src/main/java/com/shade/platform/ui/controls/plaf/FlatFileChooserUI.java b/modules/platform-ui/src/main/java/com/shade/platform/ui/controls/plaf/FlatFileChooserUI.java new file mode 100644 index 000000000..8635bd33f --- /dev/null +++ b/modules/platform-ui/src/main/java/com/shade/platform/ui/controls/plaf/FlatFileChooserUI.java @@ -0,0 +1,70 @@ +package com.shade.platform.ui.controls.plaf; + +import com.shade.platform.ui.controls.FileChooser; +import com.shade.util.NotNull; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; +import javax.swing.plaf.ComponentUI; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class FlatFileChooserUI extends com.formdev.flatlaf.ui.FlatFileChooserUI { + private JComponent optionsPanel; + + public FlatFileChooserUI(JFileChooser fc) { + super(fc); + } + + public static ComponentUI createUI(JComponent c) { + return new FlatFileChooserUI((JFileChooser) c); + } + + @Override + public void installComponents(JFileChooser fc) { + super.installComponents(fc); + + optionsPanel = new JPanel(); + optionsPanel.setLayout(new BorderLayout()); + + // Gaps and insets are defined according to MetalFileChooserUI.ButtonAreaLayout + final JPanel buttonPanel = getButtonPanel(); + buttonPanel.setLayout(new MigLayout("ins 0,gapx 5,ins 17 0 0 0", "push[][fill][fill]")); + buttonPanel.add(optionsPanel, 0); + + if (getFileChooser() instanceof FileChooser fc1 && fc1.getOptions() != null) { + optionsPanel.add(fc1.getOptions(), BorderLayout.CENTER); + } + } + + @Override + public void uninstallComponents(JFileChooser fc) { + super.uninstallComponents(fc); + + optionsPanel = null; + } + + @Override + public PropertyChangeListener createPropertyChangeListener(JFileChooser fc) { + final PropertyChangeListener parent = super.createPropertyChangeListener(fc); + return e -> { + if (e.getPropertyName().equals(FileChooser.OPTIONS_CHANGED_PROPERTY)) { + doOptionsChanged(e); + } else { + parent.propertyChange(e); + } + }; + } + + private void doOptionsChanged(@NotNull PropertyChangeEvent e) { + if (optionsPanel != null) { + if (e.getOldValue() != null) { + optionsPanel.remove((JComponent) e.getOldValue()); + } + if (e.getNewValue() != null) { + optionsPanel.add((JComponent) e.getNewValue(), BorderLayout.CENTER); + } + } + } +} diff --git a/modules/platform-ui/src/main/resources/themes/FlatLaf.properties b/modules/platform-ui/src/main/resources/themes/FlatLaf.properties index 15740edea..5fe3c1877 100644 --- a/modules/platform-ui/src/main/resources/themes/FlatLaf.properties +++ b/modules/platform-ui/src/main/resources/themes/FlatLaf.properties @@ -17,6 +17,7 @@ ToolTipUI = com.shade.platform.ui.controls.plaf.FlatOutlineToolTipUI ToolTabbedPaneUI = com.shade.platform.ui.controls.plaf.FlatToolTabbedPaneUI LabeledSeparatorUI = com.shade.platform.ui.controls.plaf.LabeledSeparatorUI ThinSplitPaneUI = com.shade.platform.ui.controls.plaf.FlatThinSplitPaneUI +FileChooserUI = com.shade.platform.ui.controls.plaf.FlatFileChooserUI #General TitlePane.unifiedBackground = false