diff --git a/pdfsam-core/pom.xml b/pdfsam-core/pom.xml index 53bc5dc8..79072689 100644 --- a/pdfsam-core/pom.xml +++ b/pdfsam-core/pom.xml @@ -54,9 +54,12 @@ org.pdfsam pdfsam-injector + - io.reactivex.rxjava3 - rxjava + org.pdfsam + pdfsam-test + ${project.version} + test diff --git a/pdfsam-core/src/main/java/module-info.java b/pdfsam-core/src/main/java/module-info.java index 44994abe..7d21cb56 100644 --- a/pdfsam-core/src/main/java/module-info.java +++ b/pdfsam-core/src/main/java/module-info.java @@ -23,7 +23,6 @@ requires org.sejda.commons; requires org.slf4j; - requires transitive io.reactivex.rxjava3; requires transitive java.xml; requires transitive javafx.graphics; requires transitive org.apache.commons.lang3; diff --git a/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationContext.java b/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationContext.java index 19b43088..715f7414 100644 --- a/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationContext.java +++ b/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationContext.java @@ -18,7 +18,6 @@ */ package org.pdfsam.core.context; -import io.reactivex.rxjava3.disposables.CompositeDisposable; import javafx.application.ConditionalFeature; import javafx.application.Platform; import javafx.scene.Scene; @@ -47,7 +46,6 @@ public class ApplicationContext implements Closeable { private final ApplicationPersistentSettings persistentSettings; private ApplicationRuntimeState runtimeState; private Optional injector = Optional.empty(); - private CompositeDisposable disposable = new CompositeDisposable(); private ApplicationContext() { this(new ApplicationPersistentSettings(new PreferencesRepository("/org/pdfsam/user/conf")), null); @@ -84,11 +82,11 @@ public ApplicationRuntimeState runtimeState() { if (Objects.isNull(this.runtimeState)) { this.runtimeState = new ApplicationRuntimeState(); //listen for changes in the working path - disposable.add(this.persistentSettings().settingsChanges(WORKING_PATH).subscribe(path -> { + this.persistentSettings().settingsChanges(WORKING_PATH).subscribe(path -> { this.runtimeState.defaultWorkingPath( path.filter(StringUtils::isNotBlank).map(Paths::get).filter(Files::isDirectory) .orElse(null)); - })); + }); var workingPath = persistentSettings().get(WORKING_PATH).filter(StringUtils::isNotBlank).map(Paths::get) .filter(Files::isDirectory).orElse(null); @@ -104,18 +102,20 @@ public ApplicationRuntimeState runtimeState() { * @param scene */ public void registerScene(Scene scene) { - disposable.add(this.runtimeState().theme().subscribe(t -> { - Platform.runLater(() -> { - scene.getStylesheets().setAll(t.stylesheets()); - if (!Platform.isSupported(ConditionalFeature.TRANSPARENT_WINDOW)) { - scene.getStylesheets().addAll(t.transparentIncapableStylesheets()); - } - }); - })); - disposable.add(this.persistentSettings().settingsChanges(FONT_SIZE).subscribe(size -> { + this.runtimeState().theme().subscribe(t -> { + if (Objects.nonNull(t)) { + Platform.runLater(() -> { + scene.getStylesheets().setAll(t.stylesheets()); + if (!Platform.isSupported(ConditionalFeature.TRANSPARENT_WINDOW)) { + scene.getStylesheets().addAll(t.transparentIncapableStylesheets()); + } + }); + } + }); + this.persistentSettings().settingsChanges(FONT_SIZE).subscribe(size -> { size.filter(StringUtils::isNotBlank).map(s -> String.format("-fx-font-size: %s;", s)) .ifPresentOrElse(scene.getRoot()::setStyle, () -> scene.getRoot().setStyle("")); - })); + }); this.persistentSettings().get(FONT_SIZE).filter(not(String::isBlank)) .ifPresent(size -> scene.getRoot().setStyle(String.format("-fx-font-size: %s;", size))); @@ -148,9 +148,6 @@ public void clean() { @Override public void close() { injector.ifPresent(Injector::close); - runtimeState().close(); - persistentSettings.close(); - disposable.dispose(); } } diff --git a/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationPersistentSettings.java b/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationPersistentSettings.java index 22c66cb4..46201c4a 100644 --- a/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationPersistentSettings.java +++ b/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationPersistentSettings.java @@ -18,9 +18,8 @@ */ package org.pdfsam.core.context; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.subjects.PublishSubject; -import io.reactivex.rxjava3.subjects.Subject; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import org.pdfsam.persistence.PersistenceException; import org.pdfsam.persistence.PreferencesRepository; import org.slf4j.Logger; @@ -30,6 +29,7 @@ import java.util.Optional; import static java.util.Objects.nonNull; +import static java.util.Optional.empty; import static java.util.Optional.of; import static java.util.Optional.ofNullable; import static org.sejda.commons.util.RequireUtils.requireNotNullArg; @@ -39,14 +39,14 @@ * * @author Andrea Vacondio */ -public class ApplicationPersistentSettings implements AutoCloseable { +public class ApplicationPersistentSettings { private static final Logger LOG = LoggerFactory.getLogger(ApplicationPersistentSettings.class); private final PreferencesRepository repo; - private final Subject> stringSettingsChanges = PublishSubject.create(); - private final Subject> intSettingsChanges = PublishSubject.create(); - private final Subject> boolSettingsChanges = PublishSubject.create(); + private final SimpleObjectProperty> stringSettingsChanges = new SimpleObjectProperty<>(); + private final SimpleObjectProperty> intSettingsChanges = new SimpleObjectProperty<>(); + private final SimpleObjectProperty> boolSettingsChanges = new SimpleObjectProperty<>(); ApplicationPersistentSettings(PreferencesRepository repo) { this.repo = repo; @@ -101,7 +101,7 @@ public void set(StringPersistentProperty prop, String value) { requireNotNullArg(prop, "Cannot set value for a null property"); try { this.repo.saveString(prop.key(), value); - stringSettingsChanges.onNext(new PersistentPropertyChange<>(prop, ofNullable(value))); + stringSettingsChanges.set(new PersistentPropertyChange<>(prop, ofNullable(value))); } catch (PersistenceException e) { LOG.error("Unable to save persistent property", e); } @@ -115,7 +115,7 @@ public void set(IntegerPersistentProperty prop, int value) { requireNotNullArg(prop, "Cannot set value for a null property"); try { this.repo.saveInt(prop.key(), value); - intSettingsChanges.onNext(new PersistentPropertyChange<>(prop, of(value))); + intSettingsChanges.set(new PersistentPropertyChange<>(prop, of(value))); } catch (PersistenceException e) { LOG.error("Unable to save persistent property", e); } @@ -128,7 +128,7 @@ public void set(BooleanPersistentProperty prop, boolean value) { requireNotNullArg(prop, "Cannot set value for a null property"); try { this.repo.saveBoolean(prop.key(), value); - boolSettingsChanges.onNext(new PersistentPropertyChange<>(prop, of(value))); + boolSettingsChanges.set(new PersistentPropertyChange<>(prop, of(value))); } catch (PersistenceException e) { LOG.error("Unable to save persistent property", e); } @@ -159,22 +159,40 @@ public void delete(PersistentProperty property) { /** * @return an observable for changes to the given property */ - public Observable> settingsChanges(StringPersistentProperty prop) { - return stringSettingsChanges.hide().filter(c -> c.property().equals(prop)).map(PersistentPropertyChange::value); + public ObservableValue> settingsChanges(StringPersistentProperty prop) { + var value = new SimpleObjectProperty>(empty()); + stringSettingsChanges.subscribe((old, c) -> { + if (c.property().equals(prop)) { + value.set(c.value()); + } + }); + return value; } /** * @return an observable for changes to the given property */ - public Observable> settingsChanges(IntegerPersistentProperty prop) { - return intSettingsChanges.hide().filter(c -> c.property().equals(prop)).map(PersistentPropertyChange::value); + public ObservableValue> settingsChanges(IntegerPersistentProperty prop) { + var value = new SimpleObjectProperty>(empty()); + intSettingsChanges.subscribe((old, c) -> { + if (c.property().equals(prop)) { + value.set(c.value()); + } + }); + return value; } /** * @return an observable for changes to the given property */ - public Observable> settingsChanges(BooleanPersistentProperty prop) { - return boolSettingsChanges.hide().filter(c -> c.property().equals(prop)).map(PersistentPropertyChange::value); + public ObservableValue> settingsChanges(BooleanPersistentProperty prop) { + var value = new SimpleObjectProperty>(empty()); + boolSettingsChanges.subscribe((old, c) -> { + if (c.property().equals(prop)) { + value.set(c.value()); + } + }); + return value; } /** @@ -189,10 +207,4 @@ public void clean() { } } - @Override - public void close() { - stringSettingsChanges.onComplete(); - intSettingsChanges.onComplete(); - boolSettingsChanges.onComplete(); - } } diff --git a/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationRuntimeState.java b/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationRuntimeState.java index 93249df1..4c58cc3f 100644 --- a/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationRuntimeState.java +++ b/pdfsam-core/src/main/java/org/pdfsam/core/context/ApplicationRuntimeState.java @@ -18,9 +18,8 @@ */ package org.pdfsam.core.context; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.subjects.BehaviorSubject; -import io.reactivex.rxjava3.subjects.ReplaySubject; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import org.apache.commons.lang3.StringUtils; import org.pdfsam.model.tool.Tool; import org.pdfsam.theme.Theme; @@ -49,14 +48,14 @@ * * @author Andrea Vacondio */ -public class ApplicationRuntimeState implements AutoCloseable { +public class ApplicationRuntimeState { private static final Logger LOG = LoggerFactory.getLogger(ApplicationRuntimeState.class); private Path defaultWorkingPath; - private final BehaviorSubject> workingPath = BehaviorSubject.createDefault(empty()); - private final BehaviorSubject> activeTool = BehaviorSubject.createDefault(empty()); - private final ReplaySubject theme = ReplaySubject.create(1); + private final SimpleObjectProperty> workingPath = new SimpleObjectProperty<>(empty()); + private final SimpleObjectProperty> activeTool = new SimpleObjectProperty<>(empty()); + private final SimpleObjectProperty theme = new SimpleObjectProperty<>(); private final Future> tools; ApplicationRuntimeState() { @@ -72,7 +71,7 @@ public class ApplicationRuntimeState implements AutoCloseable { * @param path the current working directory or the parent in case of regular file. A null value clears the current working path */ void workingPath(Path path) { - workingPath.onNext(ofNullable(path).map(p -> { + workingPath.set(ofNullable(path).map(p -> { if (Files.isRegularFile(p)) { return p.getParent(); } @@ -110,8 +109,8 @@ public Optional workingPathValue() { return workingPath.getValue(); } - public Observable> workingPath() { - return workingPath.hide(); + public ObservableValue> workingPath() { + return workingPath; } /** @@ -130,7 +129,7 @@ public Map tools() { * @param activeTool the tool currently active */ public void activeTool(Tool activeTool) { - this.activeTool.onNext(ofNullable(activeTool)); + this.activeTool.set(ofNullable(activeTool)); } /** @@ -140,22 +139,22 @@ public Optional activeToolValue() { return activeTool.getValue(); } - public Observable> activeTool() { - return activeTool.hide(); + public ObservableValue> activeTool() { + return activeTool; } /** * @return the current theme */ - public Observable theme() { - return this.theme.hide(); + public ObservableValue theme() { + return this.theme; } /** * Sets the application theme */ public void theme(Theme theme) { - ofNullable(theme).ifPresent(this.theme::onNext); + ofNullable(theme).ifPresent(this.theme::set); } void defaultWorkingPath(Path defaultWorkingPath) { @@ -163,10 +162,4 @@ void defaultWorkingPath(Path defaultWorkingPath) { workingPath(defaultWorkingPath); } - @Override - public void close() { - workingPath.onComplete(); - theme.onComplete(); - activeTool.onComplete(); - } } diff --git a/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationContextTest.java b/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationContextTest.java index 759078b4..ce1be7f6 100644 --- a/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationContextTest.java +++ b/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationContextTest.java @@ -6,11 +6,14 @@ import org.junit.jupiter.api.io.TempDir; import org.pdfsam.injector.Injector; import org.pdfsam.persistence.PreferencesRepository; +import org.pdfsam.test.ValuesRecorder; import java.nio.file.Path; +import java.util.Optional; import static java.util.Optional.empty; import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -53,9 +56,10 @@ void runtimeStateIsCreated(@TempDir Path tempDir) { @Test void runtimeWorkingPathIsBoundToPersistentSetting(@TempDir Path tempDir) { var victim = new ApplicationContext(persistentSettings, null); - var testListener = victim.runtimeState().workingPath().test(); + var values = new ValuesRecorder>(); + victim.runtimeState().workingPath().subscribe(values); victim.persistentSettings().set(StringPersistentProperty.WORKING_PATH, tempDir.toString()); - testListener.assertValuesOnly(empty(), of(tempDir)); + assertThat(values.values()).containsExactly(empty(), of(tempDir)); } @Test @@ -66,16 +70,6 @@ void clean() { verify(persistentState).clean(); } - @Test - void close() { - var persistentState = mock(ApplicationPersistentSettings.class); - var runtimeState = mock(ApplicationRuntimeState.class); - var victim = new ApplicationContext(persistentState, runtimeState); - victim.close(); - verify(persistentState).close(); - verify(runtimeState).close(); - } - @Test void closeWithInjector() { var persistentState = mock(ApplicationPersistentSettings.class); @@ -84,8 +78,6 @@ void closeWithInjector() { var victim = new ApplicationContext(persistentState, runtimeState); victim.injector(injector); victim.close(); - verify(persistentState).close(); - verify(runtimeState).close(); verify(injector).close(); } } \ No newline at end of file diff --git a/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationPersistentSettingsTest.java b/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationPersistentSettingsTest.java index 8903e72f..189c41a6 100644 --- a/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationPersistentSettingsTest.java +++ b/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationPersistentSettingsTest.java @@ -25,11 +25,14 @@ import org.pdfsam.core.ConfigurableSystemProperty; import org.pdfsam.persistence.PersistenceException; import org.pdfsam.persistence.PreferencesRepository; +import org.pdfsam.test.ValuesRecorder; +import java.util.Optional; import java.util.function.Supplier; import static java.util.Optional.empty; import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -59,38 +62,42 @@ public void setUp() { @Test @DisplayName("String value is set and notified") public void setString() throws PersistenceException { - var testListener = victim.settingsChanges(StringPersistentProperty.LOCALE).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(StringPersistentProperty.LOCALE).subscribe(values); victim.set(StringPersistentProperty.LOCALE, "it"); verify(repo).saveString(StringPersistentProperty.LOCALE.key(), "it"); - testListener.assertValue(of("it")); + assertThat(values.values()).containsExactly(empty(), of("it")); } @Test @DisplayName("Null string value is set and notified") public void setNullString() throws PersistenceException { - var testListener = victim.settingsChanges(StringPersistentProperty.LOCALE).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(StringPersistentProperty.LOCALE).subscribe(values); victim.set(StringPersistentProperty.LOCALE, null); verify(repo).saveString(StringPersistentProperty.LOCALE.key(), null); - testListener.assertValue(empty()); + assertThat(values.values()).containsExactly(empty()); } @Test @DisplayName("Blank string value is set and notified") public void setBlankString() throws PersistenceException { - var testListener = victim.settingsChanges(StringPersistentProperty.LOCALE).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(StringPersistentProperty.LOCALE).subscribe(values); victim.set(StringPersistentProperty.LOCALE, " "); verify(repo).saveString(StringPersistentProperty.LOCALE.key(), " "); - testListener.assertValue(of(" ")); + assertThat(values.values()).containsExactly(empty(), of(" ")); } @Test @DisplayName("Failing repo string value is not notified") public void negativeSetString() throws PersistenceException { doThrow(PersistenceException.class).when(repo).saveString(StringPersistentProperty.LOCALE.key(), "it"); - var testListener = victim.settingsChanges(StringPersistentProperty.LOCALE).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(StringPersistentProperty.LOCALE).subscribe(values); victim.set(StringPersistentProperty.LOCALE, "it"); verify(repo).saveString(StringPersistentProperty.LOCALE.key(), "it"); - testListener.assertNoValues(); + assertThat(values.values()).containsExactly(empty()); } @Test @@ -102,20 +109,22 @@ public void nullSetString() { @Test @DisplayName("Integer value is set and notified") public void setInteger() throws PersistenceException { - var testListener = victim.settingsChanges(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER).subscribe(values); victim.set(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER, 14); verify(repo).saveInt(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER.key(), 14); - testListener.assertValue(of(14)); + assertThat(values.values()).containsExactly(empty(), of(14)); } @Test @DisplayName("Failing repo integer value is not notified") public void negativeSetInteger() throws PersistenceException { doThrow(PersistenceException.class).when(repo).saveInt(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER.key(), 14); - var testListener = victim.settingsChanges(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER).subscribe(values); victim.set(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER, 14); verify(repo).saveInt(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER.key(), 14); - testListener.assertNoValues(); + assertThat(values.values()).containsExactly(empty()); } @Test @@ -127,10 +136,11 @@ public void nullSetInteger() { @Test @DisplayName("Boolean value is set and notified") public void setBoolean() throws PersistenceException { - var testListener = victim.settingsChanges(BooleanPersistentProperty.OVERWRITE_OUTPUT).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(BooleanPersistentProperty.OVERWRITE_OUTPUT).subscribe(values); victim.set(BooleanPersistentProperty.OVERWRITE_OUTPUT, true); verify(repo).saveBoolean(BooleanPersistentProperty.OVERWRITE_OUTPUT.key(), true); - testListener.assertValue(of(true)); + assertThat(values.values()).containsExactly(empty(), of(true)); } @Test @@ -138,10 +148,11 @@ public void setBoolean() throws PersistenceException { public void negativeSetBoolean() throws PersistenceException { doThrow(PersistenceException.class).when(repo) .saveBoolean(BooleanPersistentProperty.OVERWRITE_OUTPUT.key(), true); - var testListener = victim.settingsChanges(BooleanPersistentProperty.OVERWRITE_OUTPUT).test(); + var values = new ValuesRecorder>(); + victim.settingsChanges(BooleanPersistentProperty.OVERWRITE_OUTPUT).subscribe(values); victim.set(BooleanPersistentProperty.OVERWRITE_OUTPUT, true); verify(repo).saveBoolean(BooleanPersistentProperty.OVERWRITE_OUTPUT.key(), true); - testListener.assertNoValues(); + assertThat(values.values()).containsExactly(empty()); } @Test @@ -224,17 +235,6 @@ public void clean() throws PersistenceException { verify(repo).clean(); } - @Test - public void close() { - var intSettings = victim.settingsChanges(IntegerPersistentProperty.LOGVIEW_ROWS_NUMBER).test(); - var stringSettings = victim.settingsChanges(StringPersistentProperty.LOCALE).test(); - var booleanSettings = victim.settingsChanges(BooleanPersistentProperty.CHECK_FOR_NEWS).test(); - victim.close(); - intSettings.assertComplete(); - stringSettings.assertComplete(); - booleanSettings.assertComplete(); - } - @Test void hasValueFor() { when(repo.keys()).thenReturn(new String[] {}); diff --git a/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationRuntimeStateTest.java b/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationRuntimeStateTest.java index 822617a5..7382cbfb 100644 --- a/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationRuntimeStateTest.java +++ b/pdfsam-core/src/test/java/org/pdfsam/core/context/ApplicationRuntimeStateTest.java @@ -25,13 +25,18 @@ import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.pdfsam.model.tool.Tool; +import org.pdfsam.test.ValuesRecorder; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Optional; import static java.util.Optional.empty; import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Andrea Vacondio @@ -49,106 +54,136 @@ public void setUp() { @Test @DisplayName("Existing working Path") public void positiveMaybeWorkingPath(@TempDir Path tempDir) { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.maybeWorkingPath(tempDir); - testListener.assertValuesOnly(empty(), of(tempDir)); + assertThat(values.values()).containsExactly(empty(), of(tempDir)); } @Test @DisplayName("Existing working path as String") public void positiveMaybeWorkingPathString(@TempDir Path tempDir) { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.maybeWorkingPath(tempDir.toString()); - testListener.assertValuesOnly(empty(), of(tempDir)); + assertThat(values.values()).containsExactly(empty(), of(tempDir)); } @Test @DisplayName("Null working Path") public void nullMaybeWorkingPath(@TempDir Path tempDir) { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.maybeWorkingPath(tempDir); victim.maybeWorkingPath((Path) null); - testListener.assertValuesOnly(empty(), of(tempDir), empty()); + assertThat(values.values()).containsExactly(empty(), of(tempDir), empty()); } @Test @DisplayName("Null working path as String") public void nullMaybeWorkingPathString(@TempDir Path tempDir) { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.maybeWorkingPath(tempDir); victim.maybeWorkingPath((String) null); - testListener.assertValuesOnly(empty(), of(tempDir), empty()); + assertThat(values.values()).containsExactly(empty(), of(tempDir), empty()); } @Test @DisplayName("Blank working path as String") public void blankMaybeWorkingPathString(@TempDir Path tempDir) { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.maybeWorkingPath(tempDir); victim.maybeWorkingPath(" "); - testListener.assertValuesOnly(empty(), of(tempDir), empty()); + assertThat(values.values()).containsExactly(empty(), of(tempDir), empty()); } @Test @DisplayName("File working Path") public void fileMaybeWorkingPath(@TempDir Path tempDir) throws IOException { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.maybeWorkingPath(tempDir); - var another = Files.createTempFile(tempDir, "test", ".tmp"); + var another = Files.createTempFile("test", ".tmp"); victim.maybeWorkingPath(another); - testListener.assertValuesOnly(empty(), of(tempDir), of(another.getParent())); + assertThat(values.values()).containsExactly(empty(), of(tempDir), of(another.getParent())); } @Test @DisplayName("Default working Path") public void defaultWorkingPath(@TempDir Path tempDir) { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.defaultWorkingPath(tempDir); - testListener.assertValuesOnly(empty(), of(tempDir)); + assertThat(values.values()).containsExactly(empty(), of(tempDir)); + } + + @Test + @DisplayName("Non regular file") + public void fileWorkingPath(@TempDir Path tempDir) { + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); + victim.workingPath(tempDir); + victim.workingPath(tempDir.resolve("test.tmp")); + assertThat(values.values()).containsExactly(empty(), of(tempDir), empty()); } @Test @DisplayName("File working Path with default already set") public void maybeWorkingPathWithDefault(@TempDir Path tempDir) throws IOException { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.defaultWorkingPath(tempDir); var another = Files.createTempFile(tempDir, "test", ".tmp"); victim.maybeWorkingPath(another); - testListener.assertValuesOnly(empty(), of(tempDir)); + assertThat(values.values()).containsExactly(empty(), of(tempDir)); } @Test @DisplayName("File working String with default already set") public void maybeWorkingPathStringWithDefault(@TempDir Path tempDir) throws IOException { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.defaultWorkingPath(tempDir); var another = Files.createTempFile(tempDir, "test", ".tmp"); victim.maybeWorkingPath(another.toString()); - testListener.assertValuesOnly(empty(), of(tempDir)); + assertThat(values.values()).containsExactly(empty(), of(tempDir)); } @Test @DisplayName("Removing default working Path") public void removeWorkingPathWithDefault(@TempDir Path tempDir) throws IOException { - var testListener = victim.workingPath().test(); + var values = new ValuesRecorder>(); + victim.workingPath().subscribe(values); victim.defaultWorkingPath(tempDir); var another = Files.createTempFile(tempDir, "test", ".tmp"); victim.maybeWorkingPath(another); - testListener.assertValuesOnly(empty(), of(tempDir)); - testListener.dispose(); - var anotherListener = victim.workingPath().test(); + assertThat(values.values()).containsExactly(empty(), of(tempDir)); + values.clear(); victim.defaultWorkingPath(null); victim.maybeWorkingPath(another); - anotherListener.assertValuesOnly(of(tempDir), empty(), of(another.getParent())); + assertThat(values.values()).containsExactly(empty(), of(another.getParent())); } @Test - public void close() { - var testListenerTheme = victim.theme().test(); - var testListenerWorkingPath = victim.workingPath().test(); - victim.close(); - testListenerTheme.assertComplete(); - testListenerWorkingPath.assertComplete(); + @DisplayName("Active tool notified") + public void positiveActiveTool() { + var values = new ValuesRecorder>(); + victim.activeTool().subscribe(values); + var tool = mock(Tool.class); + victim.activeTool(tool); + assertThat(values.values()).containsExactly(empty(), of(tool)); } + @Test + @DisplayName("Null active tool") + public void nullActiveTool() { + var values = new ValuesRecorder>(); + victim.activeTool().subscribe(values); + var tool = mock(Tool.class); + victim.activeTool(tool); + victim.activeTool(null); + assertThat(values.values()).containsExactly(empty(), of(tool), empty()); + } } diff --git a/pdfsam-gui/src/main/java/org/pdfsam/gui/components/content/preference/PreferenceConfig.java b/pdfsam-gui/src/main/java/org/pdfsam/gui/components/content/preference/PreferenceConfig.java index 3ddea2f8..dc566e24 100644 --- a/pdfsam-gui/src/main/java/org/pdfsam/gui/components/content/preference/PreferenceConfig.java +++ b/pdfsam-gui/src/main/java/org/pdfsam/gui/components/content/preference/PreferenceConfig.java @@ -19,6 +19,7 @@ package org.pdfsam.gui.components.content.preference; import jakarta.inject.Named; +import javafx.util.Subscription; import org.pdfsam.core.context.StringPersistentProperty; import org.pdfsam.core.support.validation.Validators; import org.pdfsam.gui.components.content.log.MaxLogRowsChangedEvent; @@ -37,6 +38,7 @@ import java.util.stream.IntStream; import static java.util.Comparator.comparing; +import static java.util.Objects.nonNull; import static java.util.Optional.ofNullable; import static org.pdfsam.core.context.ApplicationContext.app; import static org.pdfsam.core.context.BooleanPersistentProperty.CHECK_FOR_NEWS; @@ -96,11 +98,15 @@ public PreferenceComboBox> themeCombo() { Themes.themes().entrySet().stream().sorted(Comparator.comparing(e -> e.getValue().name())) .map(entry -> new ComboItem<>(entry.getKey(), entry.getValue().name())) .forEach(themeCombo.getItems()::add); - app().runtimeState().theme().take(1).subscribe(t -> { - themeCombo.setValue(new ComboItem<>(t.id(), t.name())); - themeCombo.valueProperty().addListener( - (observable, oldVal, newVal) -> ofNullable(Themes.get(newVal.key())).ifPresent( - theme -> app().runtimeState().theme(theme))); + final Subscription[] subscription = new Subscription[1]; + subscription[0] = app().runtimeState().theme().subscribe(t -> { + if (nonNull(t)) { + themeCombo.setValue(new ComboItem<>(t.id(), t.name())); + themeCombo.valueProperty().addListener( + (observable, oldVal, newVal) -> ofNullable(Themes.get(newVal.key())).ifPresent( + theme -> app().runtimeState().theme(theme))); + ofNullable(subscription[0]).ifPresent(Subscription::unsubscribe); + } }); return themeCombo; } diff --git a/pdfsam-i18n/pom.xml b/pdfsam-i18n/pom.xml index 01b5007b..7d5b8240 100644 --- a/pdfsam-i18n/pom.xml +++ b/pdfsam-i18n/pom.xml @@ -73,8 +73,8 @@ eventstudio - io.reactivex.rxjava3 - rxjava + org.openjfx + javafx-base diff --git a/pdfsam-i18n/src/main/java/module-info.java b/pdfsam-i18n/src/main/java/module-info.java index 361852cf..ee3853e5 100644 --- a/pdfsam-i18n/src/main/java/module-info.java +++ b/pdfsam-i18n/src/main/java/module-info.java @@ -21,6 +21,6 @@ requires org.pdfsam.eventstudio; requires org.sejda.commons; - requires transitive io.reactivex.rxjava3; + requires transitive javafx.base; requires org.slf4j; } \ No newline at end of file diff --git a/pdfsam-i18n/src/main/java/org/pdfsam/i18n/I18nContext.java b/pdfsam-i18n/src/main/java/org/pdfsam/i18n/I18nContext.java index d9ad0664..09cd8d92 100644 --- a/pdfsam-i18n/src/main/java/org/pdfsam/i18n/I18nContext.java +++ b/pdfsam-i18n/src/main/java/org/pdfsam/i18n/I18nContext.java @@ -18,15 +18,14 @@ */ package org.pdfsam.i18n; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.subjects.ReplaySubject; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; import org.pdfsam.eventstudio.annotation.EventListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.MessageFormat; import java.util.Locale; -import java.util.Objects; import java.util.Optional; import java.util.ResourceBundle; import java.util.Set; @@ -53,31 +52,33 @@ public final class I18nContext { Locale.of("sl"), Locale.of("sr"), Locale.of("sv"), Locale.of("es"), Locale.of("tr"), Locale.of("uk"), Locale.of("fi"), Locale.of("ko"), Locale.of("oc")); - private final ReplaySubject locale = ReplaySubject.createWithSize(1); - + private final SimpleObjectProperty locale = new SimpleObjectProperty<>(); private Optional bundle = empty(); I18nContext() { eventStudio().addAnnotatedListeners(this); - locale.filter(Objects::nonNull) - .subscribe(this::loadBundle, e -> LOG.error("Unable to load translations bundle", e)); + locale.subscribe(this::loadBundles); } @EventListener public void setLocale(SetLocaleRequest e) { if (nonNull(e.languageTag()) && !e.languageTag().isBlank()) { LOG.trace("Setting locale to {}", e.languageTag()); - ofNullable(Locale.forLanguageTag(e.languageTag())).filter(supported::contains).ifPresent(locale::onNext); + ofNullable(Locale.forLanguageTag(e.languageTag())).filter(supported::contains).ifPresent(locale::set); } } - private void loadBundle(Locale l) { + private void loadBundles(Locale l) { if (nonNull(l)) { Locale.setDefault(l); LOG.trace("Loading i18n bundle for {}", Locale.getDefault()); - this.bundle = ofNullable(ResourceBundle.getBundle("org.pdfsam.i18n.Messages", Locale.getDefault(), - I18nContext.class.getModule())); - LOG.debug("Locale set to {}", Locale.getDefault()); + try { + this.bundle = ofNullable(ResourceBundle.getBundle("org.pdfsam.i18n.Messages", Locale.getDefault(), + I18nContext.class.getModule())); + LOG.debug("Locale set to {}", Locale.getDefault()); + } catch (Exception e) { + LOG.error("Unable to load translations bundle", e); + } } } @@ -103,11 +104,10 @@ public static I18nContext i18n() { } /** - * @return an {@link Observable} {@link Locale} representing the current locale + * @return an {@link ObservableValue} {@link Locale} representing the current locale */ - public Observable locale() { - return this.locale.hide(); - + public ObservableValue locale() { + return this.locale; } public String tr(String text) { @@ -127,7 +127,7 @@ public String tr(String text, String... replace) { private void initBundleIfRequired() { if (bundle.isEmpty()) { - locale.onNext(getBestLocale()); + locale.set(getBestLocale()); } } diff --git a/pdfsam-i18n/src/test/java/org/pdfsam/i18n/I18NContextTest.java b/pdfsam-i18n/src/test/java/org/pdfsam/i18n/I18NContextTest.java index ccf0521c..f3978a27 100644 --- a/pdfsam-i18n/src/test/java/org/pdfsam/i18n/I18NContextTest.java +++ b/pdfsam-i18n/src/test/java/org/pdfsam/i18n/I18NContextTest.java @@ -19,10 +19,13 @@ package org.pdfsam.i18n; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Locale; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.pdfsam.eventstudio.StaticStudio.eventStudio; /** @@ -30,53 +33,46 @@ */ public class I18NContextTest { - @AfterEach - public void tearDown() { - eventStudio().clear(); + private I18nContext victim; + + @BeforeEach + public void before() { + this.victim = new I18nContext(); } - @Test - public void setInvalidLanguageTag() { - Locale.setDefault(Locale.UK); - var observer = new I18nContext().locale().test(); - eventStudio().broadcast(new SetLocaleRequest("Chuck Norris")); - observer.assertNoValues(); + @AfterEach + public void after() { + eventStudio().clear(); } @Test public void setLocaleSupported() { - var observer = new I18nContext().locale().test(); eventStudio().broadcast(new SetLocaleRequest("it")); - observer.assertValue(Locale.ITALIAN); + assertEquals(Locale.ITALIAN, victim.locale().getValue()); + } @Test public void getBestLocaleSupported() { - var victim = new I18nContext(); Locale.setDefault(Locale.ITALIAN); - var observer = victim.locale().test(); - observer.assertNoValues(); + assertNull(victim.locale().getValue()); victim.tr("chuck norris"); - observer.assertValue(Locale.ITALIAN); + assertEquals(Locale.ITALIAN, victim.locale().getValue()); } @Test public void getBestLocaleSupportedLanguage() { - var victim = new I18nContext(); Locale.setDefault(Locale.of("en", "CA")); - var observer = victim.locale().test(); - observer.assertNoValues(); + assertNull(victim.locale().getValue()); victim.tr("chuck norris"); - observer.assertValue(Locale.ENGLISH); + assertEquals(Locale.ENGLISH, victim.locale().getValue()); } @Test public void getBestLocaleNotSupportedLanguage() { - var victim = new I18nContext(); - Locale.setDefault(Locale.CANADA_FRENCH); - var observer = victim.locale().test(); - observer.assertNoValues(); + Locale.setDefault(Locale.of("mn", "MN")); + assertNull(victim.locale().getValue()); victim.tr("chuck norris"); - observer.assertValue(Locale.FRENCH); + assertEquals(Locale.ENGLISH, victim.locale().getValue()); } } diff --git a/pdfsam-test/src/main/java/org/pdfsam/test/ValuesRecorder.java b/pdfsam-test/src/main/java/org/pdfsam/test/ValuesRecorder.java index d056da3d..39eff047 100644 --- a/pdfsam-test/src/main/java/org/pdfsam/test/ValuesRecorder.java +++ b/pdfsam-test/src/main/java/org/pdfsam/test/ValuesRecorder.java @@ -38,4 +38,8 @@ public void accept(T t) { public List values() { return values; } + + public void clear() { + values.clear(); + } } diff --git a/pdfsam-ui-components/pom.xml b/pdfsam-ui-components/pom.xml index c170c08d..2fa79eb7 100644 --- a/pdfsam-ui-components/pom.xml +++ b/pdfsam-ui-components/pom.xml @@ -40,6 +40,11 @@ pdfsam-persistence ${project.version} + + org.pdfsam + pdfsam-model + ${project.version} + org.apache.commons commons-lang3 diff --git a/pom.xml b/pom.xml index 0f161fe0..de88d9ec 100644 --- a/pom.xml +++ b/pom.xml @@ -218,7 +218,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.12.1 + 3.13.0 21 @@ -243,7 +243,7 @@ org.apache.maven.plugins maven-assembly-plugin - 3.6.0 + 3.7.1 org.apache.maven.plugins @@ -310,23 +310,22 @@ 1.2.1 2.0.11 3.14.0 - 2.15.1 - 5.10.0 + 2.16.1 + 5.11.0 2.7.0 7.0.5.Final - 5.0.13 + 5.0.15 4.0.0.M3 3.0.0 4.0.0 5.0.0 - 3.1.8 12.3.1 4.0.17 2.16.1 - 1.77 + 1.78 3.25.3 2.2 - 21.0.2 + 21.0.3 4.0.2 2.0.1 3.0.2 @@ -444,11 +443,6 @@ jakarta.inject-api ${jakarta.inject.version} - - io.reactivex.rxjava3 - rxjava - ${rxjava.version} - org.tinylog slf4j-tinylog