Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* This file is part of the PDF Split And Merge source code
* Created on 13/nov/2025
* Copyright 2025 by Sober Lemur S.r.l. (info@soberlemur.com).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.pdfsam.model.ui;

import java.io.File;

import static org.sejda.commons.util.RequireUtils.requireNotNullArg;

/**
* Request to set an output directory using the given file as footprint.
* The directory will be created based on the input file name (without extension).
*
* @author Andrea Vacondio
*/
public record SetOutputDirectoryRequest(File directory, boolean fallback) {

/**
* @param footprint input file to use for generating the output directory name
* @return a request to set the output directory based on the footprint file name
*/
public static SetOutputDirectoryRequest requestOutputDirectory(File footprint) {
requireNotNullArg(footprint, "Footprint file cannot be null");
String nameWithoutExtension = getNameWithoutExtension(footprint.getName());
return new SetOutputDirectoryRequest(new File(footprint.getParent(), nameWithoutExtension), false);
}

/**
* @param footprint input file to use for generating the output directory name
* @return a request to set the output directory as fallback based on the footprint file name
*/
public static SetOutputDirectoryRequest requestFallbackOutputDirectory(File footprint) {
requireNotNullArg(footprint, "Footprint file cannot be null");
String nameWithoutExtension = getNameWithoutExtension(footprint.getName());
return new SetOutputDirectoryRequest(new File(footprint.getParent(), nameWithoutExtension), true);
}

/**
* Removes the file extension from a filename
*/
private static String getNameWithoutExtension(String filename) {
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex > 0) {
return filename.substring(0, lastDotIndex);
}
return filename;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static class ModuleConfig {
@Provides
@Named(TOOL_ID + "field")
public BrowsableOutputDirectoryField destinationDirectoryField() {
return new BrowsableOutputDirectoryField();
return new BrowsableOutputDirectoryField(TOOL_ID);
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static class ModuleConfig {
@Provides
@Named(TOOL_ID + "field")
public BrowsableOutputDirectoryField destinationDirectoryField() {
return new BrowsableOutputDirectoryField();
return new BrowsableOutputDirectoryField(TOOL_ID);
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static class ModuleConfig {
@Provides
@Named(TOOL_ID + "field")
public BrowsableOutputDirectoryField destinationDirectoryField() {
BrowsableOutputDirectoryField field = new BrowsableOutputDirectoryField();
BrowsableOutputDirectoryField field = new BrowsableOutputDirectoryField(TOOL_ID);
field.setId(TOOL_ID + "field");
return field;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,29 @@
*/
package org.pdfsam.ui.components.io;

import javafx.scene.control.Label;
import org.pdfsam.core.context.ApplicationContext;
import org.pdfsam.core.context.BooleanPersistentProperty;
import org.pdfsam.core.support.params.MultipleOutputTaskParametersBuilder;
import org.pdfsam.core.support.params.TaskParametersBuildStep;
import org.pdfsam.model.ui.NonExistingOutputDirectoryEvent;
import org.pdfsam.eventstudio.annotation.EventListener;
import org.pdfsam.eventstudio.annotation.EventStation;
import org.pdfsam.model.tool.ToolBound;
import org.pdfsam.model.ui.SetOutputDirectoryRequest;
import org.pdfsam.ui.components.support.FXValidationSupport;
import org.pdfsam.ui.components.support.Style;
import org.sejda.model.parameter.base.SingleOrMultipleOutputTaskParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.function.Consumer;

import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.pdfsam.core.context.ApplicationContext.app;
import static org.pdfsam.core.context.StringPersistentProperty.WORKING_PATH;
import static org.pdfsam.core.support.validation.Validators.and;
Expand All @@ -43,15 +55,72 @@
* @author Andrea Vacondio
*/
public class BrowsableOutputDirectoryField extends BrowsableDirectoryField
implements TaskParametersBuildStep<MultipleOutputTaskParametersBuilder<?>> {
implements TaskParametersBuildStep<MultipleOutputTaskParametersBuilder<?>>, ToolBound {

private static final Logger LOG = LoggerFactory.getLogger(BrowsableOutputDirectoryField.class);
private String ownerModule = "";
private final Label infoLabel = new Label();

public BrowsableOutputDirectoryField() {
this(app());
this(app(), "");
}

public BrowsableOutputDirectoryField(String ownerModule) {
this(app(), ownerModule);
}

BrowsableOutputDirectoryField(ApplicationContext context) {
this(context, "");
}

BrowsableOutputDirectoryField(ApplicationContext context, String ownerModule) {
super();
this.ownerModule = defaultString(ownerModule);
eventStudio().addAnnotatedListeners(this);
context.persistentSettings().get(WORKING_PATH).ifPresent(getTextField()::setText);
getTextField().setValidator(and(nonBlank(), v -> !Files.isRegularFile(Paths.get(v))));

// Setup info label
infoLabel.getStyleClass().addAll("info-message");
infoLabel.setStyle("-fx-text-fill: #2E7D32; -fx-padding: 2 0 0 2;");
infoLabel.setManaged(false);
infoLabel.setVisible(false);

// Add listener to show/hide info message
getTextField().textProperty().addListener((obs, oldVal, newVal) -> updateInfoLabel());
}

private void updateInfoLabel() {
if (isNotBlank(getTextField().getText())) {
var path = Paths.get(getTextField().getText());
if (!Files.exists(path)) {
infoLabel.setText(i18n().tr("Folder will be created"));
infoLabel.setManaged(true);
infoLabel.setVisible(true);
return;
}
}
infoLabel.setManaged(false);
infoLabel.setVisible(false);
}

public Label getInfoLabel() {
return infoLabel;
}

@EventListener
public void setOutputDirectory(SetOutputDirectoryRequest event) {
if (!event.fallback() || isBlank(getTextField().getText()) || app().persistentSettings()
.get(BooleanPersistentProperty.SMART_OUTPUT)) {
getTextField().setText(event.directory().getAbsolutePath());
updateInfoLabel();
}
}

@Override
@EventStation
public String toolBinding() {
return ownerModule;
}

@Override
Expand All @@ -60,9 +129,19 @@ public void apply(MultipleOutputTaskParametersBuilder<? extends SingleOrMultiple
getTextField().validate();
if (getTextField().getValidationState() == FXValidationSupport.ValidationState.VALID) {
var output = Paths.get(getTextField().getText());

// Auto-create directory if it doesn't exist
if (!Files.exists(output)) {
eventStudio().broadcast(new NonExistingOutputDirectoryEvent(output));
try {
Files.createDirectories(output);
LOG.debug("Created output directory {}", output);
} catch (IOException e) {
LOG.warn("Unable to create output directory", e);
onError.accept(i18n().tr("Unable to create output directory: {0}", e.getMessage()));
return;
}
}

if (Files.isDirectory(output)) {
builder.output(directory(output.toFile()));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@ public DestinationPane(BrowsableField destination) {
overwrite.getStyleClass().addAll(Style.WITH_HELP.css());

// destination.getStyleClass().addAll(Style.VITEM.css());
getChildren().addAll(destination, overwrite);
getChildren().add(destination);

// Add info label for BrowsableOutputDirectoryField
if (destination instanceof BrowsableOutputDirectoryField dirField) {
getChildren().add(dirField.getInfoLabel());
}

getChildren().add(overwrite);
getStyleClass().addAll(Style.CONTAINER.css());
getStyleClass().addAll(Style.VCONTAINER.css());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import static org.pdfsam.model.pdf.PdfDocumentDescriptor.newDescriptorNoPassword;
import static org.pdfsam.model.ui.SetDestinationRequest.requestDestination;
import static org.pdfsam.model.ui.SetDestinationRequest.requestFallbackDestination;
import static org.pdfsam.model.ui.SetOutputDirectoryRequest.requestFallbackOutputDirectory;

/**
* Panel letting the user select a single input PDF document
Expand All @@ -103,6 +104,7 @@ public class SingleSelectionPane extends VBox implements ToolBound, PdfDocumentD

private Consumer<PdfDocumentDescriptor> onLoaded = d -> {
eventStudio().broadcast(requestFallbackDestination(d.getFile(), toolBinding()), toolBinding());
eventStudio().broadcast(requestFallbackOutputDirectory(d.getFile()), toolBinding());
eventStudio().broadcast(new ChangedSelectedPdfVersionEvent(d.getVersion()), toolBinding());
};

Expand Down