From fac45dd2b226c3bc123a1011b3ec52d26de38c8c Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Wed, 6 Aug 2025 16:52:45 +0200 Subject: [PATCH 01/10] Moved all the GithubLogic shared between App and WebApp to a shared class. Introduced pattern, on how to share logic between App and WebApp. Possible with the final target, to merge them together. --- .../src/main/java/dev/ikm/komet/app/App.java | 279 ++--------------- .../java/dev/ikm/komet/app/AppGithub.java | 288 ++++++++++++++++++ .../java/dev/ikm/komet/app/AppInterface.java | 25 ++ .../main/java/dev/ikm/komet/app/WebApp.java | 281 ++--------------- 4 files changed, 354 insertions(+), 519 deletions(-) create mode 100644 application/src/main/java/dev/ikm/komet/app/AppGithub.java create mode 100644 application/src/main/java/dev/ikm/komet/app/AppInterface.java diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 4d267feb8..20890086f 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -134,7 +134,7 @@ /** * JavaFX App */ -public class App extends Application { +public class App extends Application implements AppInterface { private static final String OS_NAME_MAC = "mac"; private static final String CHANGESETS_DIR = "changeSets"; @@ -154,6 +154,17 @@ public class App extends Application { private EvtBus kViewEventBus; private static Stage landingPageWindow; + AppGithub appGithub; + + @Override + public LandingPageController getLandingPageController() { + return landingPageController; + } + + @Override + public GitHubPreferencesDao getGitHubPreferencesDao() { + return gitHubPreferencesDao; + } /** * An entry point to launch the newer UI panels. @@ -289,6 +300,8 @@ public void init() { @Override public void start(Stage stage) { + appGithub = new AppGithub(this); + try { App.primaryStage = stage; Thread.currentThread().setUncaughtExceptionHandler((t, e) -> AlertStreams.getRoot().dispatch(AlertObject.makeError(e))); @@ -423,7 +436,7 @@ private void launchLandingPage() { } landingPageController = landingPageLoader.getController(); landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); - landingPageController.getGithubStatusHyperlink().setOnAction(_ -> connectToGithub()); + landingPageController.getGithubStatusHyperlink().setOnAction(_ -> appGithub.connectToGithub()); Scene sourceScene = new Scene(landingPageBorderPane, 1200, 800); kViewStage.setScene(sourceScene); @@ -599,7 +612,7 @@ private void saveJournalWindowsToPreferences() { public void stop() { LOG.info("Stopping application\n\n###############\n\n"); - disconnectFromGithub(); + appGithub.disconnectFromGithub(); // close all journal windows Lists.immutable.ofAll(this.journalControllersList).forEach(JournalController::close); @@ -786,258 +799,6 @@ private void openExport() { exportStage.show(); } - /** - * Prompts the user for GitHub preferences and repository information. - *

- * This method displays a dialog where the user can enter their GitHub credentials - * and repository URL. It creates a CompletableFuture that will be resolved with - * {@code true} if the user successfully enters valid credentials and connects, - * or {@code false} if they cancel the operation. - * - * @return A CompletableFuture that completes with true if valid GitHub preferences - * were successfully provided by the user, or false if the user canceled the operation - */ - private CompletableFuture promptForGitHubPrefs() { - // Create a CompletableFuture that will be completed when the user makes a choice - CompletableFuture future = new CompletableFuture<>(); - - // Show dialog on JavaFX thread - runOnFxThread(() -> { - GlassPane glassPane = new GlassPane(landingPageController.getRoot()); - - final JFXNode githubPreferencesNode = FXMLMvvmLoader - .make(GitHubPreferencesController.class.getResource("github-preferences.fxml")); - final Pane dialogPane = githubPreferencesNode.node(); - final GitHubPreferencesController controller = githubPreferencesNode.controller(); - Optional githubPrefsViewModelOpt = githubPreferencesNode - .getViewModel("gitHubPreferencesViewModel"); - - controller.getConnectButton().setOnAction(actionEvent -> { - controller.handleConnectButtonEvent(actionEvent); - githubPrefsViewModelOpt.ifPresent(githubPrefsViewModel -> { - if (githubPrefsViewModel.validProperty().get()) { - glassPane.removeContent(dialogPane); - glassPane.hide(); - future.complete(true); // Complete with true on successful connection - } - }); - }); - - controller.getCancelButton().setOnAction(_ -> { - glassPane.removeContent(dialogPane); - glassPane.hide(); - future.complete(false); // Complete with false on cancel - }); - - glassPane.addContent(dialogPane); - glassPane.show(); - }); - - return future; - } - - /** - * Sets up the UI to reflect a disconnected GitHub state. - *

- * This method updates the GitHub status hyperlink in the landing page to show that - * the application is disconnected from GitHub. When clicked, the hyperlink will - * attempt to connect to GitHub by calling the {@link #connectToGithub()} method. - *

- * The method runs on the JavaFX application thread to ensure thread safety when - * updating UI components. - */ - private void gotoGitHubDisconnectedState() { - runOnFxThread(() -> { - if (landingPageController != null) { - Hyperlink githubStatusHyperlink = landingPageController.getGithubStatusHyperlink(); - githubStatusHyperlink.setText("Disconnected, Select to connect"); - githubStatusHyperlink.setOnAction(event -> connectToGithub()); - } - }); - } - - /** - * Sets up the UI to reflect a connected GitHub state. - *

- * This method updates the GitHub status hyperlink in the landing page to show that - * the application is successfully connected to GitHub. When clicked, the hyperlink - * will disconnect from GitHub by calling the {@link #disconnectFromGithub()} method. - *

- * The method runs on the JavaFX application thread to ensure thread safety when - * updating UI components. - */ - private void gotoGitHubConnectedState() { - runOnFxThread(() -> { - if (landingPageController != null) { - Hyperlink githubStatusHyperlink = landingPageController.getGithubStatusHyperlink(); - githubStatusHyperlink.setText("Connected"); - githubStatusHyperlink.setOnAction(event -> disconnectFromGithub()); - } - }); - } - - /** - * Executes a Git task, ensuring preferences are valid first. - *

- * This method performs the following operations: - *

    - *
  1. Verifies that the data store root is available
  2. - *
  3. Creates a changeset folder if it doesn't exist
  4. - *
  5. Validates GitHub preferences and prompts for them if missing
  6. - *
  7. Creates and runs the appropriate GitTask based on the operation mode
  8. - *
- * If GitHub preferences are missing or invalid, this method will prompt the user - * to enter them before proceeding with the requested operation. - * - * @param mode The operation mode (CONNECT, PULL, or SYNC) that determines what - * Git operations will be performed - */ - private void executeGitTask(OperationMode mode) { - Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); - if (optionalDataStoreRoot.isEmpty()) { - LOG.error("ServiceKeys.DATA_STORE_ROOT not provided."); - return; - } - - final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); - if (!changeSetFolder.exists()) { - if (!changeSetFolder.mkdirs()) { - LOG.error("Unable to create {} directory", CHANGESETS_DIR); - return; - } - } - - // Check if GitHub preferences are valid first - if (!gitHubPreferencesDao.validate()) { - LOG.info("GitHub preferences missing or incomplete. Prompting user..."); - - // Prompt for preferences before proceeding - promptForGitHubPrefs().thenAccept(confirmed -> { - if (confirmed) { - // Preferences entered successfully, now run the GitTask - createAndRunGitTask(mode, changeSetFolder); - } else { - LOG.info("User cancelled the GitHub preferences dialog"); - } - }); - } else { - // Preferences already valid, run the GitTask directly - createAndRunGitTask(mode, changeSetFolder); - } - } - - /** - * Creates and runs a GitTask with the specified operation mode. - *

- * This helper method is called after GitHub preferences have been validated. It: - *

    - *
  1. Creates a new GitTask with the specified operation mode
  2. - *
  3. Registers a success callback to update the UI state
  4. - *
  5. Runs the task with progress tracking
  6. - *
  7. Handles errors and updates the UI accordingly
  8. - *
- * The task is executed asynchronously through the ProgressHelper service to - * provide user feedback during long-running operations. - * - * @param operationMode The operation mode specifying which Git operations to perform - * @param changeSetFolder The folder where the Git repository is located - * @return A CompletableFuture that completes with true if the operation was successful, - * or false if it failed or was cancelled - */ - private CompletableFuture createAndRunGitTask(OperationMode operationMode, File changeSetFolder) { - // Create a GitTask with only the connection success callback - GitTask gitTask = new GitTask(operationMode, changeSetFolder.toPath(), this::gotoGitHubConnectedState); - - // Run the task - return ProgressHelper.progress(LANDING_PAGE_TOPIC, gitTask) - .whenComplete((result, throwable) -> { - if (throwable != null) { - LOG.error("Error during {} operation", operationMode, throwable); - disconnectFromGithub(); - } else if (!result) { - LOG.warn("{} operation did not complete successfully", operationMode); - disconnectFromGithub(); - } - }); - } - - /** - * Initiates a connection to GitHub. - *

- * This method establishes a connection to GitHub by executing a GitTask in CONNECT mode. - * If successful, the UI will be updated to reflect the connected state, and the local - * Git repository will be initialized and configured with the remote origin. - *

- * If GitHub preferences are missing or invalid, the user will be prompted to - * enter them before the connection is established. - */ - private void connectToGithub() { - LOG.info("Attempting to connect to GitHub..."); - executeGitTask(CONNECT); - } - - /** - * Disconnects from GitHub and cleans up local resources. - *

- * This method performs the following cleanup operations: - *

    - *
  1. Logs the disconnection attempt
  2. - *
  3. Removes all GitHub-related preferences from user preferences
  4. - *
  5. Deletes the local .git repository folder if it exists
  6. - *
  7. Deletes the README.md file if it exists
  8. - *
  9. Updates the UI to reflect the disconnected state
  10. - *
- * If any errors occur during this process, they are logged but do not prevent - * the disconnection from completing. - */ - private void disconnectFromGithub() { - LOG.info("Disconnecting from GitHub..."); - - // Delete stored user preferences related to GitHub - try { - gitHubPreferencesDao.delete(); - LOG.info("Successfully deleted GitHub preferences"); - } catch (BackingStoreException e) { - LOG.error("Failed to delete GitHub preferences", e); - } - // TODO: Refactor GitHub disconnect and credentials management without deleting .git folder -// // Delete the .git folder and README.md if they exist -// Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); -// if (optionalDataStoreRoot.isPresent()) { -// final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); -// -// // Delete .git folder -// final File gitDir = new File(changeSetFolder, ".git"); -// if (gitDir.exists() && gitDir.isDirectory()) { -// try { -// FileUtils.delete(gitDir, FileUtils.RECURSIVE); -// LOG.info("Successfully deleted .git folder at: {}", gitDir.getAbsolutePath()); -// } catch (IOException e) { -// LOG.error("Failed to delete .git folder at: {}", gitDir.getAbsolutePath(), e); -// } -// } -// -// // Delete README.md file -// final File readmeFile = new File(changeSetFolder, README_FILENAME); -// if (readmeFile.exists() && readmeFile.isFile()) { -// try { -// if (readmeFile.delete()) { -// LOG.info("Successfully deleted {} file at: {}", README_FILENAME, readmeFile.getAbsolutePath()); -// } else { -// LOG.error("Failed to delete {} file at: {}", README_FILENAME, readmeFile.getAbsolutePath()); -// } -// } catch (SecurityException e) { -// LOG.error("Security exception while deleting {} file at: {}", readmeFile.getAbsolutePath(), e); -// } -// } -// } else { -// LOG.warn("Could not access data store root to delete .git folder and README.md"); -// } -// - // Update the UI state - gotoGitHubDisconnectedState(); - } - /** * Displays information about the current Git repository. *

@@ -1068,10 +829,10 @@ private void infoAction() { fetchAndShowRepositoryInfo(changeSetFolder); } else { // Prompt for preferences before proceeding - promptForGitHubPrefs().thenCompose(confirmed -> { + appGithub.promptForGitHubPrefs().thenCompose(confirmed -> { if (confirmed) { // Preferences entered successfully, now run the GitTask - return createAndRunGitTask(CONNECT, changeSetFolder); + return appGithub.createAndRunGitTask(CONNECT, changeSetFolder); } else { return CompletableFuture.completedFuture(false); } @@ -1218,9 +979,9 @@ private Menu createExchangeMenu() { MenuItem infoMenuItem = new MenuItem("Info"); infoMenuItem.setOnAction(actionEvent -> infoAction()); MenuItem pullMenuItem = new MenuItem("Pull"); - pullMenuItem.setOnAction(actionEvent -> executeGitTask(PULL)); + pullMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(PULL)); MenuItem pushMenuItem = new MenuItem("Sync"); - pushMenuItem.setOnAction(actionEvent -> executeGitTask(SYNC)); + pushMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(SYNC)); exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); return exchangeMenu; diff --git a/application/src/main/java/dev/ikm/komet/app/AppGithub.java b/application/src/main/java/dev/ikm/komet/app/AppGithub.java new file mode 100644 index 000000000..cf179ce98 --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppGithub.java @@ -0,0 +1,288 @@ +package dev.ikm.komet.app; + +import dev.ikm.komet.framework.progress.ProgressHelper; +import dev.ikm.komet.kview.controls.GlassPane; +import dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitHubPreferencesController; +import dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask; +import dev.ikm.komet.kview.mvvm.viewmodel.GitHubPreferencesViewModel; +import dev.ikm.tinkar.common.service.ServiceKeys; +import dev.ikm.tinkar.common.service.ServiceProperties; +import javafx.scene.control.Hyperlink; +import javafx.scene.layout.Pane; +import org.carlfx.cognitive.loader.FXMLMvvmLoader; +import org.carlfx.cognitive.loader.JFXNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.prefs.BackingStoreException; + +import static dev.ikm.komet.framework.events.FrameworkTopics.LANDING_PAGE_TOPIC; +import static dev.ikm.komet.kview.fxutils.FXUtils.runOnFxThread; +import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.CONNECT; + +public class AppGithub { + + private static final Logger LOG = LoggerFactory.getLogger(AppGithub.class); + static final String CHANGESETS_DIR = "changeSets"; + + private final AppInterface app; + + public AppGithub(AppInterface app) { + this.app = app; + } + + /** + * Prompts the user for GitHub preferences and repository information. + *

+ * This method displays a dialog where the user can enter their GitHub credentials + * and repository URL. It creates a CompletableFuture that will be resolved with + * {@code true} if the user successfully enters valid credentials and connects, + * or {@code false} if they cancel the operation. + * + * @return A CompletableFuture that completes with true if valid GitHub preferences + * were successfully provided by the user, or false if the user canceled the operation + */ + CompletableFuture promptForGitHubPrefs() { + // Create a CompletableFuture that will be completed when the user makes a choice + CompletableFuture future = new CompletableFuture<>(); + + // Show dialog on JavaFX thread + runOnFxThread(() -> { + GlassPane glassPane = new GlassPane(app.getLandingPageController().getRoot()); + + final JFXNode githubPreferencesNode = FXMLMvvmLoader + .make(GitHubPreferencesController.class.getResource("github-preferences.fxml")); + final Pane dialogPane = githubPreferencesNode.node(); + final GitHubPreferencesController controller = githubPreferencesNode.controller(); + Optional githubPrefsViewModelOpt = githubPreferencesNode + .getViewModel("gitHubPreferencesViewModel"); + + controller.getConnectButton().setOnAction(actionEvent -> { + controller.handleConnectButtonEvent(actionEvent); + githubPrefsViewModelOpt.ifPresent(githubPrefsViewModel -> { + if (githubPrefsViewModel.validProperty().get()) { + glassPane.removeContent(dialogPane); + glassPane.hide(); + future.complete(true); // Complete with true on successful connection + } + }); + }); + + controller.getCancelButton().setOnAction(_ -> { + glassPane.removeContent(dialogPane); + glassPane.hide(); + future.complete(false); // Complete with false on cancel + }); + + glassPane.addContent(dialogPane); + glassPane.show(); + }); + + return future; + } + + /** + * Sets up the UI to reflect a disconnected GitHub state. + *

+ * This method updates the GitHub status hyperlink in the landing page to show that + * the application is disconnected from GitHub. When clicked, the hyperlink will + * attempt to connect to GitHub by calling the {@link #connectToGithub()} method. + *

+ * The method runs on the JavaFX application thread to ensure thread safety when + * updating UI components. + */ + private void gotoGitHubDisconnectedState() { + runOnFxThread(() -> { + if (app.getLandingPageController() != null) { + Hyperlink githubStatusHyperlink = app.getLandingPageController().getGithubStatusHyperlink(); + githubStatusHyperlink.setText("Disconnected, Select to connect"); + githubStatusHyperlink.setOnAction(event -> connectToGithub()); + } + }); + } + + /** + * Sets up the UI to reflect a connected GitHub state. + *

+ * This method updates the GitHub status hyperlink in the landing page to show that + * the application is successfully connected to GitHub. When clicked, the hyperlink + * will disconnect from GitHub by calling the {@link #disconnectFromGithub()} method. + *

+ * The method runs on the JavaFX application thread to ensure thread safety when + * updating UI components. + */ + private void gotoGitHubConnectedState() { + runOnFxThread(() -> { + if (app.getLandingPageController() != null) { + Hyperlink githubStatusHyperlink = app.getLandingPageController().getGithubStatusHyperlink(); + githubStatusHyperlink.setText("Connected"); + githubStatusHyperlink.setOnAction(event -> disconnectFromGithub()); + } + }); + } + + /** + * Executes a Git task, ensuring preferences are valid first. + *

+ * This method performs the following operations: + *

    + *
  1. Verifies that the data store root is available
  2. + *
  3. Creates a changeset folder if it doesn't exist
  4. + *
  5. Validates GitHub preferences and prompts for them if missing
  6. + *
  7. Creates and runs the appropriate GitTask based on the operation mode
  8. + *
+ * If GitHub preferences are missing or invalid, this method will prompt the user + * to enter them before proceeding with the requested operation. + * + * @param mode The operation mode (CONNECT, PULL, or SYNC) that determines what + * Git operations will be performed + */ + void executeGitTask(GitTask.OperationMode mode) { + Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); + if (optionalDataStoreRoot.isEmpty()) { + LOG.error("ServiceKeys.DATA_STORE_ROOT not provided."); + return; + } + + final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); + if (!changeSetFolder.exists()) { + if (!changeSetFolder.mkdirs()) { + LOG.error("Unable to create {} directory", CHANGESETS_DIR); + return; + } + } + + // Check if GitHub preferences are valid first + if (!app.getGitHubPreferencesDao().validate()) { + LOG.info("GitHub preferences missing or incomplete. Prompting user..."); + + // Prompt for preferences before proceeding + promptForGitHubPrefs().thenAccept(confirmed -> { + if (confirmed) { + // Preferences entered successfully, now run the GitTask + createAndRunGitTask(mode, changeSetFolder); + } else { + LOG.info("User cancelled the GitHub preferences dialog"); + } + }); + } else { + // Preferences already valid, run the GitTask directly + createAndRunGitTask(mode, changeSetFolder); + } + } + + /** + * Creates and runs a GitTask with the specified operation mode. + *

+ * This helper method is called after GitHub preferences have been validated. It: + *

    + *
  1. Creates a new GitTask with the specified operation mode
  2. + *
  3. Registers a success callback to update the UI state
  4. + *
  5. Runs the task with progress tracking
  6. + *
  7. Handles errors and updates the UI accordingly
  8. + *
+ * The task is executed asynchronously through the ProgressHelper service to + * provide user feedback during long-running operations. + * + * @param operationMode The operation mode specifying which Git operations to perform + * @param changeSetFolder The folder where the Git repository is located + * @return A CompletableFuture that completes with true if the operation was successful, + * or false if it failed or was cancelled + */ + CompletableFuture createAndRunGitTask(GitTask.OperationMode operationMode, File changeSetFolder) { + // Create a GitTask with only the connection success callback + GitTask gitTask = new GitTask(operationMode, changeSetFolder.toPath(), this::gotoGitHubConnectedState); + + // Run the task + return ProgressHelper.progress(LANDING_PAGE_TOPIC, gitTask) + .whenComplete((result, throwable) -> { + if (throwable != null) { + LOG.error("Error during {} operation", operationMode, throwable); + disconnectFromGithub(); + } else if (!result) { + LOG.warn("{} operation did not complete successfully", operationMode); + disconnectFromGithub(); + } + }); + } + + /** + * Initiates a connection to GitHub. + *

+ * This method establishes a connection to GitHub by executing a GitTask in CONNECT mode. + * If successful, the UI will be updated to reflect the connected state, and the local + * Git repository will be initialized and configured with the remote origin. + *

+ * If GitHub preferences are missing or invalid, the user will be prompted to + * enter them before the connection is established. + */ + void connectToGithub() { + LOG.info("Attempting to connect to GitHub..."); + executeGitTask(CONNECT); + } + + /** + * Disconnects from GitHub and cleans up local resources. + *

+ * This method performs the following cleanup operations: + *

    + *
  1. Logs the disconnection attempt
  2. + *
  3. Removes all GitHub-related preferences from user preferences
  4. + *
  5. Deletes the local .git repository folder if it exists
  6. + *
  7. Deletes the README.md file if it exists
  8. + *
  9. Updates the UI to reflect the disconnected state
  10. + *
+ * If any errors occur during this process, they are logged but do not prevent + * the disconnection from completing. + */ + void disconnectFromGithub() { + LOG.info("Disconnecting from GitHub..."); + + // Delete stored user preferences related to GitHub + try { + app.getGitHubPreferencesDao().delete(); + LOG.info("Successfully deleted GitHub preferences"); + } catch (BackingStoreException e) { + LOG.error("Failed to delete GitHub preferences", e); + } + // TODO: Refactor GitHub disconnect and credentials management without deleting .git folder +// // Delete the .git folder and README.md if they exist +// Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); +// if (optionalDataStoreRoot.isPresent()) { +// final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); +// +// // Delete .git folder +// final File gitDir = new File(changeSetFolder, ".git"); +// if (gitDir.exists() && gitDir.isDirectory()) { +// try { +// FileUtils.delete(gitDir, FileUtils.RECURSIVE); +// LOG.info("Successfully deleted .git folder at: {}", gitDir.getAbsolutePath()); +// } catch (IOException e) { +// LOG.error("Failed to delete .git folder at: {}", gitDir.getAbsolutePath(), e); +// } +// } +// +// // Delete README.md file +// final File readmeFile = new File(changeSetFolder, README_FILENAME); +// if (readmeFile.exists() && readmeFile.isFile()) { +// try { +// if (readmeFile.delete()) { +// LOG.info("Successfully deleted {} file at: {}", README_FILENAME, readmeFile.getAbsolutePath()); +// } else { +// LOG.error("Failed to delete {} file at: {}", README_FILENAME, readmeFile.getAbsolutePath()); +// } +// } catch (SecurityException e) { +// LOG.error("Security exception while deleting {} file at: {}", readmeFile.getAbsolutePath(), e); +// } +// } +// } else { +// LOG.warn("Could not access data store root to delete .git folder and README.md"); +// } +// + // Update the UI state + gotoGitHubDisconnectedState(); + } +} diff --git a/application/src/main/java/dev/ikm/komet/app/AppInterface.java b/application/src/main/java/dev/ikm/komet/app/AppInterface.java new file mode 100644 index 000000000..130ba6f32 --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppInterface.java @@ -0,0 +1,25 @@ +package dev.ikm.komet.app; + +import dev.ikm.komet.kview.mvvm.model.GitHubPreferencesDao; +import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController; + +/** + * Contains the shared elements between App and WebApp. + * Ideally, this will be removed and only one App is left - because they become the same. + */ +public interface AppInterface { + + /** + * Returns the controller for the landing page. + * + * @return the {@link LandingPageController} instance + */ + LandingPageController getLandingPageController(); + + /** + * Returns the GitHub preferences DAO. + * + * @return the {@link GitHubPreferencesDao} instance + */ + GitHubPreferencesDao getGitHubPreferencesDao(); +} diff --git a/application/src/main/java/dev/ikm/komet/app/WebApp.java b/application/src/main/java/dev/ikm/komet/app/WebApp.java index e05b9a7f6..1f62ff290 100644 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ b/application/src/main/java/dev/ikm/komet/app/WebApp.java @@ -160,10 +160,9 @@ * @see LoginFeatureFlag * @see KometPreferences */ -public class WebApp extends Application { +public class WebApp extends Application implements AppInterface { private static final Logger LOG = LoggerFactory.getLogger(WebApp.class); - private static final String CHANGESETS_DIR = "changeSets"; public static final String ICON_LOCATION = "/icons/Komet.png"; public static final SimpleObjectProperty state = new SimpleObjectProperty<>(STARTING); public static final SimpleObjectProperty userProperty = new SimpleObjectProperty<>(); @@ -183,6 +182,17 @@ public class WebApp extends Application { private LandingPageController landingPageController; private EvtBus kViewEventBus; + AppGithub appGithub; + + @Override + public LandingPageController getLandingPageController() { + return landingPageController; + } + @Override + public GitHubPreferencesDao getGitHubPreferencesDao() { + return gitHubPreferencesDao; + } + /** * An entry point to launch the newer UI panels. */ @@ -388,6 +398,8 @@ public void init() throws Exception { @Override public void start(Stage stage) { + appGithub = new AppGithub(this); + try { WebApp.primaryStage = stage; Thread.currentThread().setUncaughtExceptionHandler((thread, exception) -> @@ -476,7 +488,7 @@ private void startSelectDataSource(Stage stage) { public void stop() { LOG.info("Stopping application\n\n###############\n\n"); - disconnectFromGithub(); + appGithub.disconnectFromGithub(); if (IS_DESKTOP) { // close all journal windows @@ -635,9 +647,9 @@ private Menu createExchangeMenu() { MenuItem infoMenuItem = new MenuItem("Info"); infoMenuItem.setOnAction(actionEvent -> infoAction()); MenuItem pullMenuItem = new MenuItem("Pull"); - pullMenuItem.setOnAction(actionEvent -> executeGitTask(PULL)); + pullMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(PULL)); MenuItem pushMenuItem = new MenuItem("Sync"); - pushMenuItem.setOnAction(actionEvent -> executeGitTask(SYNC)); + pushMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(SYNC)); exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); return exchangeMenu; @@ -711,7 +723,7 @@ private void launchLandingPage(Stage stage, User user) { landingPageController = landingPageLoader.getController(); landingPageController.getWelcomeTitleLabel().setText("Welcome " + user.getName()); landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); - landingPageController.getGithubStatusHyperlink().setOnAction(_ -> connectToGithub()); + landingPageController.getGithubStatusHyperlink().setOnAction(_ -> appGithub.connectToGithub()); stage.setTitle("Landing Page"); stage.setMaximized(true); @@ -1061,257 +1073,6 @@ private void openExport(Window owner) { exportStage.show(); } - /** - * Prompts the user for GitHub preferences and repository information. - *

- * This method displays a dialog where the user can enter their GitHub credentials - * and repository URL. It creates a CompletableFuture that will be resolved with - * {@code true} if the user successfully enters valid credentials and connects, - * or {@code false} if they cancel the operation. - * - * @return A CompletableFuture that completes with true if valid GitHub preferences - * were successfully provided by the user, or false if the user canceled the operation - */ - private CompletableFuture promptForGitHubPrefs() { - // Create a CompletableFuture that will be completed when the user makes a choice - CompletableFuture future = new CompletableFuture<>(); - - // Show dialog on JavaFX thread - runOnFxThread(() -> { - GlassPane glassPane = new GlassPane(landingPageController.getRoot()); - - final JFXNode githubPreferencesNode = FXMLMvvmLoader - .make(GitHubPreferencesController.class.getResource("github-preferences.fxml")); - final Pane dialogPane = githubPreferencesNode.node(); - final GitHubPreferencesController controller = githubPreferencesNode.controller(); - Optional githubPrefsViewModelOpt = githubPreferencesNode - .getViewModel("gitHubPreferencesViewModel"); - - controller.getConnectButton().setOnAction(actionEvent -> { - controller.handleConnectButtonEvent(actionEvent); - githubPrefsViewModelOpt.ifPresent(githubPrefsViewModel -> { - if (githubPrefsViewModel.validProperty().get()) { - glassPane.removeContent(dialogPane); - glassPane.hide(); - future.complete(true); // Complete with true on successful connection - } - }); - }); - - controller.getCancelButton().setOnAction(_ -> { - glassPane.removeContent(dialogPane); - glassPane.hide(); - future.complete(false); // Complete with false on cancel - }); - - glassPane.addContent(dialogPane); - glassPane.show(); - }); - - return future; - } - - /** - * Sets up the UI to reflect a disconnected GitHub state. - *

- * This method updates the GitHub status hyperlink in the landing page to show that - * the application is disconnected from GitHub. When clicked, the hyperlink will - * attempt to connect to GitHub by calling the {@link #connectToGithub()} method. - *

- * The method runs on the JavaFX application thread to ensure thread safety when - * updating UI components. - */ - private void gotoGitHubDisconnectedState() { - runOnFxThread(() -> { - if (landingPageController != null) { - Hyperlink githubStatusHyperlink = landingPageController.getGithubStatusHyperlink(); - githubStatusHyperlink.setText("Disconnected, Select to connect"); - githubStatusHyperlink.setOnAction(event -> connectToGithub()); - } - }); - } - - /** - * Sets up the UI to reflect a connected GitHub state. - *

- * This method updates the GitHub status hyperlink in the landing page to show that - * the application is successfully connected to GitHub. When clicked, the hyperlink - * will disconnect from GitHub by calling the {@link #disconnectFromGithub()} method. - *

- * The method runs on the JavaFX application thread to ensure thread safety when - * updating UI components. - */ - private void gotoGitHubConnectedState() { - runOnFxThread(() -> { - if (landingPageController != null) { - Hyperlink githubStatusHyperlink = landingPageController.getGithubStatusHyperlink(); - githubStatusHyperlink.setText("Connected"); - githubStatusHyperlink.setOnAction(event -> disconnectFromGithub()); - } - }); - } - - /** - * Executes a Git task, ensuring preferences are valid first. - *

- * This method performs the following operations: - *

    - *
  1. Verifies that the data store root is available
  2. - *
  3. Creates a changeset folder if it doesn't exist
  4. - *
  5. Validates GitHub preferences and prompts for them if missing
  6. - *
  7. Creates and runs the appropriate GitTask based on the operation mode
  8. - *
- * If GitHub preferences are missing or invalid, this method will prompt the user - * to enter them before proceeding with the requested operation. - * - * @param mode The operation mode (CONNECT, PULL, or SYNC) that determines what - * Git operations will be performed - */ - private void executeGitTask(GitTask.OperationMode mode) { - Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); - if (optionalDataStoreRoot.isEmpty()) { - LOG.error("ServiceKeys.DATA_STORE_ROOT not provided."); - return; - } - - final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); - if (!changeSetFolder.exists()) { - if (!changeSetFolder.mkdirs()) { - LOG.error("Unable to create {} directory", CHANGESETS_DIR); - return; - } - } - - // Check if GitHub preferences are valid first - if (!gitHubPreferencesDao.validate()) { - LOG.info("GitHub preferences missing or incomplete. Prompting user..."); - - // Prompt for preferences before proceeding - promptForGitHubPrefs().thenAccept(confirmed -> { - if (confirmed) { - // Preferences entered successfully, now run the GitTask - createAndRunGitTask(mode, changeSetFolder); - } else { - LOG.info("User cancelled the GitHub preferences dialog"); - } - }); - } else { - // Preferences already valid, run the GitTask directly - createAndRunGitTask(mode, changeSetFolder); - } - } - - /** - * Creates and runs a GitTask with the specified operation mode. - *

- * This helper method is called after GitHub preferences have been validated. It: - *

    - *
  1. Creates a new GitTask with the specified operation mode
  2. - *
  3. Registers a success callback to update the UI state
  4. - *
  5. Runs the task with progress tracking
  6. - *
  7. Handles errors and updates the UI accordingly
  8. - *
- * The task is executed asynchronously through the ProgressHelper service to - * provide user feedback during long-running operations. - * - * @param operationMode The operation mode specifying which Git operations to perform - * @param changeSetFolder The folder where the Git repository is located - * @return A CompletableFuture that completes with true if the operation was successful, - * or false if it failed or was cancelled - */ - private CompletableFuture createAndRunGitTask(GitTask.OperationMode operationMode, File changeSetFolder) { - // Create a GitTask with only the connection success callback - GitTask gitTask = new GitTask(operationMode, changeSetFolder.toPath(), this::gotoGitHubConnectedState); - - // Run the task - return ProgressHelper.progress(LANDING_PAGE_TOPIC, gitTask) - .whenComplete((result, throwable) -> { - if (throwable != null) { - LOG.error("Error during {} operation", operationMode, throwable); - disconnectFromGithub(); - } else if (!result) { - LOG.warn("{} operation did not complete successfully", operationMode); - disconnectFromGithub(); - } - }); - } - - /** - * Initiates a connection to GitHub. - *

- * This method establishes a connection to GitHub by executing a GitTask in CONNECT mode. - * If successful, the UI will be updated to reflect the connected state, and the local - * Git repository will be initialized and configured with the remote origin. - *

- * If GitHub preferences are missing or invalid, the user will be prompted to - * enter them before the connection is established. - */ - private void connectToGithub() { - LOG.info("Attempting to connect to GitHub..."); - executeGitTask(CONNECT); - } - - /** - * Disconnects from GitHub and cleans up local resources. - *

- * This method performs the following cleanup operations: - *

    - *
  1. Logs the disconnection attempt
  2. - *
  3. Removes all GitHub-related preferences from user preferences
  4. - *
  5. Deletes the local .git repository folder if it exists
  6. - *
  7. Deletes the README.md file if it exists
  8. - *
  9. Updates the UI to reflect the disconnected state
  10. - *
- * If any errors occur during this process, they are logged but do not prevent - * the disconnection from completing. - */ - private void disconnectFromGithub() { - LOG.info("Disconnecting from GitHub..."); - - // Delete stored user preferences related to GitHub - try { - gitHubPreferencesDao.delete(); - LOG.info("Successfully deleted GitHub preferences"); - } catch (BackingStoreException e) { - LOG.error("Failed to delete GitHub preferences", e); - } - - // Delete the .git folder and README.md if they exist - Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); - if (optionalDataStoreRoot.isPresent()) { - final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); - - // Delete .git folder - final File gitDir = new File(changeSetFolder, ".git"); - if (gitDir.exists() && gitDir.isDirectory()) { - try { - FileUtils.delete(gitDir, FileUtils.RECURSIVE); - LOG.info("Successfully deleted .git folder at: {}", gitDir.getAbsolutePath()); - } catch (IOException e) { - LOG.error("Failed to delete .git folder at: {}", gitDir.getAbsolutePath(), e); - } - } - - // Delete README.md file - final File readmeFile = new File(changeSetFolder, README_FILENAME); - if (readmeFile.exists() && readmeFile.isFile()) { - try { - if (readmeFile.delete()) { - LOG.info("Successfully deleted {} file at: {}", README_FILENAME, readmeFile.getAbsolutePath()); - } else { - LOG.error("Failed to delete {} file at: {}", README_FILENAME, readmeFile.getAbsolutePath()); - } - } catch (SecurityException e) { - LOG.error("Security exception while deleting {} file at: {}", readmeFile.getAbsolutePath(), e); - } - } - } else { - LOG.warn("Could not access data store root to delete .git folder and README.md"); - } - - // Update the UI state - gotoGitHubDisconnectedState(); - } /** * Displays information about the current Git repository. @@ -1336,17 +1097,17 @@ private void infoAction() { return; } - final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); + final File changeSetFolder = new File(optionalDataStoreRoot.get(), AppGithub.CHANGESETS_DIR); final File gitDir = new File(changeSetFolder, ".git"); if (gitDir.exists()) { fetchAndShowRepositoryInfo(changeSetFolder); } else { // Prompt for preferences before proceeding - promptForGitHubPrefs().thenCompose(confirmed -> { + appGithub.promptForGitHubPrefs().thenCompose(confirmed -> { if (confirmed) { // Preferences entered successfully, now run the GitTask - return createAndRunGitTask(CONNECT, changeSetFolder); + return appGithub.createAndRunGitTask(CONNECT, changeSetFolder); } else { return CompletableFuture.completedFuture(false); } From 0cb30b5e13d1c97e4d208333e851eb2945b6755d Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Thu, 7 Aug 2025 20:22:53 +0200 Subject: [PATCH 02/10] Refactor application structure by introducing AppClassicKomet and AppMenu classes, and updating AppInterface methods for better modularity and maintainability. --- .../src/main/java/dev/ikm/komet/app/App.java | 215 +++------------ .../dev/ikm/komet/app/AppClassicKomet.java | 254 ++++++++++++++++++ .../java/dev/ikm/komet/app/AppInterface.java | 30 +++ .../main/java/dev/ikm/komet/app/AppMenu.java | 72 +++++ .../main/java/dev/ikm/komet/app/WebApp.java | 251 +++-------------- 5 files changed, 434 insertions(+), 388 deletions(-) create mode 100644 application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java create mode 100644 application/src/main/java/dev/ikm/komet/app/AppMenu.java diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 20890086f..9f9824ce3 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -15,6 +15,7 @@ */ package dev.ikm.komet.app; +import com.jpro.webapi.WebAPI; import com.sun.management.OperatingSystemMXBean; import de.jangassen.MenuToolkit; import de.jangassen.model.AppearanceMode; @@ -79,6 +80,7 @@ import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; +import javafx.scene.image.Image; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; @@ -92,6 +94,7 @@ import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.util.Duration; +import one.jpro.platform.auth.core.authentication.User; import org.carlfx.cognitive.loader.Config; import org.carlfx.cognitive.loader.FXMLMvvmLoader; import org.carlfx.cognitive.loader.JFXNode; @@ -141,11 +144,10 @@ public class App extends Application implements AppInterface { private static final Logger LOG = LoggerFactory.getLogger(App.class); public static final SimpleObjectProperty state = new SimpleObjectProperty<>(STARTING); - private static Stage primaryStage; + private Stage primaryStage; private static Stage classicKometStage; private static long windowCount = 1; - private static KometPreferencesStage kometPreferencesStage; // variables specific to resource overlay private Stage overlayStage; @@ -155,6 +157,28 @@ public class App extends Application implements AppInterface { private static Stage landingPageWindow; AppGithub appGithub; + AppClassicKomet appClassicKomet; + AppMenu appMenu; + + @Override + public AppMenu getAppMenu() { + return appMenu; + } + + @Override + public WebAPI getWebAPI() { + return null; + } + + @Override + public Image getAppIcon() { + return null; + } + + @Override + public Stage getPrimaryStage() { + return primaryStage; + } @Override public LandingPageController getLandingPageController() { @@ -166,11 +190,6 @@ public GitHubPreferencesDao getGitHubPreferencesDao() { return gitHubPreferencesDao; } - /** - * An entry point to launch the newer UI panels. - */ - private MenuItem createJournalViewMenuItem; - /** * This is a list of new windows that have been launched. During shutdown, the application close each stage gracefully. */ @@ -290,7 +309,7 @@ public void init() { .findFirst() .ifPresentOrElse( JournalController::windowToFront, /* Window already launched now make window to the front (so user sees window) */ - () -> launchJournalViewWindow(journalWindowSettingsObjectMap) /* launch new Journal view window */ + () -> launchJournalViewPage(journalWindowSettingsObjectMap) /* launch new Journal view window */ ); }; // subscribe to the topic @@ -301,9 +320,11 @@ public void init() { public void start(Stage stage) { appGithub = new AppGithub(this); + appClassicKomet = new AppClassicKomet(this); + appMenu = new AppMenu(this); try { - App.primaryStage = stage; + primaryStage = stage; Thread.currentThread().setUncaughtExceptionHandler((t, e) -> AlertStreams.getRoot().dispatch(AlertObject.makeError(e))); // Get the toolkit MenuToolkit tk = MenuToolkit.toolkit(); @@ -311,7 +332,7 @@ public void start(Stage stage) { MenuItem prefsItem = new MenuItem("Komet preferences..."); prefsItem.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN)); - prefsItem.setOnAction(event -> App.kometPreferencesStage.showPreferences()); + prefsItem.setOnAction(event -> appClassicKomet.kometPreferencesStage.showPreferences()); kometAppMenu.getItems().add(2, prefsItem); kometAppMenu.getItems().add(3, new SeparatorMenuItem()); @@ -327,11 +348,11 @@ public void start(Stage stage) { Menu fileMenu = new Menu("File"); MenuItem importDatasetMenuItem = new MenuItem("Import Dataset"); - importDatasetMenuItem.setOnAction(actionEvent -> openImport()); + importDatasetMenuItem.setOnAction(actionEvent -> openImport(primaryStage)); // Exporting data MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset"); - exportDatasetMenuItem.setOnAction(actionEvent -> openExport()); + exportDatasetMenuItem.setOnAction(actionEvent -> openExport(primaryStage)); fileMenu.getItems().add(exportDatasetMenuItem); fileMenu.getItems().addAll(importDatasetMenuItem, exportDatasetMenuItem, new SeparatorMenuItem(), tk.createCloseWindowMenuItem()); @@ -413,9 +434,9 @@ public void start(Stage stage) { } } - private void launchLandingPage() { + public void launchLandingPage(Stage stage, User user) { if (landingPageWindow != null) { - App.primaryStage = landingPageWindow; + primaryStage = landingPageWindow; landingPageWindow.show(); landingPageWindow.toFront(); landingPageWindow.setMaximized(true); @@ -470,7 +491,7 @@ private void launchLandingPage() { * * @param journalWindowSettings if present will give the size and positioning of the journal window */ - private void launchJournalViewWindow(PrefX journalWindowSettings) { + private void launchJournalViewPage(PrefX journalWindowSettings) { Objects.requireNonNull(journalWindowSettings, "journalWindowSettings cannot be null"); final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); final KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); @@ -495,7 +516,7 @@ private void launchJournalViewWindow(PrefX journalWindowSettings) { journalStageWindow.setScene(sourceScene); // if NOT on Mac OS if (System.getProperty("os.name") != null && !System.getProperty("os.name").toLowerCase().startsWith(OS_NAME_MAC)) { - generateMsWindowsMenu(journalBorderPane); + appMenu.generateMsWindowsMenu(journalBorderPane, journalStageWindow); } // load journal specific window settings @@ -665,7 +686,7 @@ private void appStateChangeListener(ObservableValue observab case RUNNING -> { primaryStage.hide(); - launchLandingPage(); + launchLandingPage(primaryStage, null); } case SHUTDOWN -> { quit(); @@ -677,85 +698,6 @@ private void appStateChangeListener(ObservableValue observab } } - private void launchClassicKomet() throws IOException, BackingStoreException { - // If already launched bring to the front - if (classicKometStage != null && classicKometStage.isShowing()) { - classicKometStage.show(); - classicKometStage.toFront(); - return; - } - classicKometStage = new Stage(); - //Starting up preferences and getting configurations - Preferences.start(); - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - boolean appInitialized = appPreferences.getBoolean(AppKeys.APP_INITIALIZED, false); - if (appInitialized) { - LOG.info("Restoring configuration preferences. "); - } else { - LOG.info("Creating new configuration preferences. "); - } - - MainWindowRecord mainWindowRecord = MainWindowRecord.make(); - - BorderPane kometRoot = mainWindowRecord.root(); - KometStageController controller = mainWindowRecord.controller(); - - //Loading/setting the Komet screen - Scene kometScene = new Scene(kometRoot, 1800, 1024); - addStylesheets(kometScene, KOMET_CSS); - - // if NOT on Mac OS - if (System.getProperty("os.name") != null && !System.getProperty("os.name").toLowerCase().startsWith(OS_NAME_MAC)) { - generateMsWindowsMenu(controller.getTopBarVBox()); - } - - classicKometStage.setScene(kometScene); - - KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - boolean mainWindowInitialized = windowPreferences.getBoolean(KometStageController.WindowKeys.WINDOW_INITIALIZED, false); - controller.setup(windowPreferences, classicKometStage); - classicKometStage.setTitle("Komet"); - - if (!mainWindowInitialized) { - controller.setLeftTabs(makeDefaultLeftTabs(controller.windowView()), 0); - controller.setCenterTabs(makeDefaultCenterTabs(controller.windowView()), 0); - controller.setRightTabs(makeDefaultRightTabs(controller.windowView()), 1); - windowPreferences.putBoolean(KometStageController.WindowKeys.WINDOW_INITIALIZED, true); - appPreferences.putBoolean(AppKeys.APP_INITIALIZED, true); - } else { - // Restore nodes from preferences. - windowPreferences.get(LEFT_TAB_PREFERENCES).ifPresent(leftTabPreferencesName -> { - restoreTab(windowPreferences, leftTabPreferencesName, controller.windowView(), - controller::leftBorderPaneSetCenter); - }); - windowPreferences.get(CENTER_TAB_PREFERENCES).ifPresent(centerTabPreferencesName -> { - restoreTab(windowPreferences, centerTabPreferencesName, controller.windowView(), - controller::centerBorderPaneSetCenter); - }); - windowPreferences.get(RIGHT_TAB_PREFERENCES).ifPresent(rightTabPreferencesName -> { - restoreTab(windowPreferences, rightTabPreferencesName, controller.windowView(), - controller::rightBorderPaneSetCenter); - }); - } - //Setting X and Y coordinates for location of the Komet stage - classicKometStage.setX(controller.windowSettings().xLocationProperty().get()); - classicKometStage.setY(controller.windowSettings().yLocationProperty().get()); - classicKometStage.setHeight(controller.windowSettings().heightProperty().get()); - classicKometStage.setWidth(controller.windowSettings().widthProperty().get()); - classicKometStage.show(); - - App.kometPreferencesStage = new KometPreferencesStage(controller.windowView().makeOverridableViewProperties()); - - windowPreferences.sync(); - appPreferences.sync(); - if (createJournalViewMenuItem != null) { - createJournalViewMenuItem.setDisable(false); - KeyCombination newJournalKeyCombo = new KeyCodeCombination(KeyCode.J, KeyCombination.SHORTCUT_DOWN); - createJournalViewMenuItem.setAccelerator(newJournalKeyCombo); - KometPreferences journalPreferences = appPreferences.node(JOURNALS); - } - } - private void openImport(FrameworkTopics destinationTopic) { KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); @@ -776,11 +718,11 @@ private void openImport(FrameworkTopics destinationTopic) { importStage.show(); } - private void openImport() { + public void openImport(Stage stage) { openImport(PROGRESS_TOPIC); } - private void openExport() { + public void openExport(Stage stage) { KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); WindowSettings windowSettings = new WindowSettings(windowPreferences); @@ -915,61 +857,7 @@ private CompletableFuture showRepositoryInfoDialog(Map showAboutDialog()); - fileMenu.getItems().add(about); - - MenuItem importMenuItem = new MenuItem("Import Dataset"); - importMenuItem.setOnAction(actionEvent -> openImport()); - fileMenu.getItems().add(importMenuItem); - - // Exporting data - MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset"); - exportDatasetMenuItem.setOnAction(actionEvent -> openExport()); - fileMenu.getItems().add(exportDatasetMenuItem); - - MenuItem menuItemQuit = new MenuItem("Quit"); - KeyCombination quitKeyCombo = new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN); - menuItemQuit.setOnAction(actionEvent -> quit()); - menuItemQuit.setAccelerator(quitKeyCombo); - fileMenu.getItems().add(menuItemQuit); - - Menu editMenu = new Menu("Edit"); - MenuItem landingPage = new MenuItem("Landing Page"); - KeyCombination landingPageKeyCombo = new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN); - landingPage.setOnAction(actionEvent -> launchLandingPage()); - landingPage.setAccelerator(landingPageKeyCombo); - editMenu.getItems().add(landingPage); - - Menu windowMenu = new Menu("Window"); - MenuItem minimizeWindow = new MenuItem("Minimize"); - KeyCombination minimizeKeyCombo = new KeyCodeCombination(KeyCode.M, KeyCombination.CONTROL_DOWN); - minimizeWindow.setOnAction(event -> { - Stage obj = (Stage) node.getScene().getWindow(); - obj.setIconified(true); - }); - minimizeWindow.setAccelerator(minimizeKeyCombo); - windowMenu.getItems().add(minimizeWindow); - - menuBar.getMenus().add(fileMenu); - menuBar.getMenus().add(editMenu); - menuBar.getMenus().add(windowMenu); - - // when we add to the journal view we are adding the menu to the top of a border pane - if (node instanceof BorderPane kometRoot) { - kometRoot.setTop(menuBar); - } else if (node instanceof VBox topBarVBox) { - // when we add to the classic Komet view, we are adding to the topGridPane, which - // is not the outer BorderPane of the window but a VBox across the top of the window - topBarVBox.getChildren().add(0, menuBar); - } - } - - private void showAboutDialog() { + public void showAboutDialog() { AboutDialog aboutDialog = new AboutDialog(); aboutDialog.showAndWait(); } @@ -987,30 +875,13 @@ private Menu createExchangeMenu() { return exchangeMenu; } - private void quit() { + public void quit() { saveJournalWindowsToPreferences(); PrimitiveData.stop(); Preferences.stop(); Platform.exit(); } - private void restoreTab(KometPreferences windowPreferences, String tabPreferenceNodeName, ObservableViewNoOverride windowView, Consumer nodeConsumer) { - LOG.info("Restoring from: " + tabPreferenceNodeName); - KometPreferences itemPreferences = windowPreferences.node(KOMET_NODES + tabPreferenceNodeName); - itemPreferences.get(WindowComponent.WindowComponentKeys.FACTORY_CLASS).ifPresent(factoryClassName -> { - try { - Class objectClass = Class.forName(factoryClassName); - Class annotationClass = Reconstructor.class; - Object[] parameters = new Object[]{windowView, itemPreferences}; - WindowComponent windowComponent = (WindowComponent) Encodable.decode(objectClass, annotationClass, parameters); - nodeConsumer.accept(windowComponent.getNode()); - - } catch (Exception e) { - AlertStreams.getRoot().dispatch(AlertObject.makeError(e)); - } - }); - } - /** * Create the menu options for the landing page. * @@ -1065,7 +936,7 @@ private MenuItem createClassicKometMenuItem() { KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN); classicKometMenuItem.setOnAction(actionEvent -> { try { - launchClassicKomet(); + appClassicKomet.launchClassicKomet(); } catch (IOException e) { throw new RuntimeException(e); } catch (BackingStoreException e) { diff --git a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java new file mode 100644 index 000000000..bfe670188 --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java @@ -0,0 +1,254 @@ +package dev.ikm.komet.app; + +import dev.ikm.komet.details.DetailsNodeFactory; +import dev.ikm.komet.framework.KometNode; +import dev.ikm.komet.framework.activity.ActivityStreamOption; +import dev.ikm.komet.framework.activity.ActivityStreams; +import dev.ikm.komet.framework.events.FrameworkTopics; +import dev.ikm.komet.framework.preferences.KometPreferencesStage; +import dev.ikm.komet.framework.preferences.Reconstructor; +import dev.ikm.komet.framework.tabs.DetachableTab; +import dev.ikm.komet.framework.view.ObservableViewNoOverride; +import dev.ikm.komet.framework.window.KometStageController; +import dev.ikm.komet.framework.window.MainWindowRecord; +import dev.ikm.komet.framework.window.WindowComponent; +import dev.ikm.komet.framework.window.WindowSettings; +import dev.ikm.komet.kview.mvvm.view.changeset.ImportController; +import dev.ikm.komet.list.ListNodeFactory; +import dev.ikm.komet.navigator.graph.GraphNavigatorNodeFactory; +import dev.ikm.komet.navigator.pattern.PatternNavigatorFactory; +import dev.ikm.komet.preferences.KometPreferences; +import dev.ikm.komet.preferences.KometPreferencesImpl; +import dev.ikm.komet.preferences.Preferences; +import dev.ikm.komet.progress.CompletionNodeFactory; +import dev.ikm.komet.progress.ProgressNodeFactory; +import dev.ikm.komet.search.SearchNodeFactory; +import dev.ikm.komet.table.TableNodeFactory; +import dev.ikm.tinkar.common.alert.AlertObject; +import dev.ikm.tinkar.common.alert.AlertStreams; +import dev.ikm.tinkar.common.binary.Encodable; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.carlfx.cognitive.loader.Config; +import org.carlfx.cognitive.loader.FXMLMvvmLoader; +import org.carlfx.cognitive.loader.JFXNode; +import org.eclipse.collections.api.factory.Lists; +import org.eclipse.collections.api.list.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.function.Consumer; +import java.util.prefs.BackingStoreException; + +import static dev.ikm.komet.app.WebApp.*; +import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; +import static dev.ikm.komet.app.util.CssUtils.addStylesheets; +import static dev.ikm.komet.framework.KometNodeFactory.KOMET_NODES; +import static dev.ikm.komet.framework.window.WindowSettings.Keys.*; +import static dev.ikm.komet.kview.fxutils.FXUtils.getFocusedWindow; +import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.VIEW_PROPERTIES; +import static dev.ikm.komet.kview.mvvm.viewmodel.ImportViewModel.ImportField.DESTINATION_TOPIC; +import static dev.ikm.komet.preferences.JournalWindowPreferences.JOURNALS; +import static dev.ikm.komet.preferences.JournalWindowPreferences.MAIN_KOMET_WINDOW; + +public class AppClassicKomet { + + private static final Logger LOG = LoggerFactory.getLogger(AppGithub.class); + + private Stage classicKometStage; + + private final AppInterface app; + + KometPreferencesStage kometPreferencesStage; + + /** + * An entry point to launch the newer UI panels. + */ + private MenuItem createJournalViewMenuItem; + + public AppClassicKomet(AppInterface app) { + this.app = app; + } + + void launchClassicKomet() throws IOException, BackingStoreException { + if (IS_DESKTOP) { + // If already launched bring to the front + if (classicKometStage != null && classicKometStage.isShowing()) { + classicKometStage.show(); + classicKometStage.toFront(); + return; + } + } + + classicKometStage = new Stage(); + classicKometStage.getIcons().setAll(app.getAppIcon()); + + //Starting up preferences and getting configurations + Preferences.start(); + KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); + boolean appInitialized = appPreferences.getBoolean(WebApp.AppKeys.APP_INITIALIZED, false); + if (appInitialized) { + LOG.info("Restoring configuration preferences."); + } else { + LOG.info("Creating new configuration preferences."); + } + + MainWindowRecord mainWindowRecord = MainWindowRecord.make(); + + BorderPane kometRoot = mainWindowRecord.root(); + KometStageController controller = mainWindowRecord.controller(); + + //Loading/setting the Komet screen + Scene kometScene = new Scene(kometRoot, 1800, 1024); + addStylesheets(kometScene, KOMET_CSS); + + // if NOT on macOS + if (!IS_MAC) { + app.getAppMenu().generateMsWindowsMenu(kometRoot, classicKometStage); + } + + classicKometStage.setScene(kometScene); + + KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); + boolean mainWindowInitialized = windowPreferences.getBoolean(KometStageController.WindowKeys.WINDOW_INITIALIZED, false); + controller.setup(windowPreferences, classicKometStage); + classicKometStage.setTitle("Komet"); + + if (!mainWindowInitialized) { + controller.setLeftTabs(makeDefaultLeftTabs(controller.windowView()), 0); + controller.setCenterTabs(makeDefaultCenterTabs(controller.windowView()), 0); + controller.setRightTabs(makeDefaultRightTabs(controller.windowView()), 1); + windowPreferences.putBoolean(KometStageController.WindowKeys.WINDOW_INITIALIZED, true); + appPreferences.putBoolean(WebApp.AppKeys.APP_INITIALIZED, true); + } else { + // Restore nodes from preferences. + windowPreferences.get(LEFT_TAB_PREFERENCES).ifPresent(leftTabPreferencesName -> + restoreTab(windowPreferences, leftTabPreferencesName, controller.windowView(), + controller::leftBorderPaneSetCenter)); + windowPreferences.get(CENTER_TAB_PREFERENCES).ifPresent(centerTabPreferencesName -> + restoreTab(windowPreferences, centerTabPreferencesName, controller.windowView(), + controller::centerBorderPaneSetCenter)); + windowPreferences.get(RIGHT_TAB_PREFERENCES).ifPresent(rightTabPreferencesName -> + restoreTab(windowPreferences, rightTabPreferencesName, controller.windowView(), + controller::rightBorderPaneSetCenter)); + } + //Setting X and Y coordinates for location of the Komet stage + classicKometStage.setX(controller.windowSettings().xLocationProperty().get()); + classicKometStage.setY(controller.windowSettings().yLocationProperty().get()); + classicKometStage.setHeight(controller.windowSettings().heightProperty().get()); + classicKometStage.setWidth(controller.windowSettings().widthProperty().get()); + classicKometStage.show(); + + if (IS_BROWSER) { + app.getWebAPI().openStageAsTab(classicKometStage); + } + + kometPreferencesStage = new KometPreferencesStage(controller.windowView().makeOverridableViewProperties()); + + windowPreferences.sync(); + appPreferences.sync(); + + if (createJournalViewMenuItem != null) { + createJournalViewMenuItem.setDisable(false); + KeyCombination newJournalKeyCombo = new KeyCodeCombination(KeyCode.J, KeyCombination.SHORTCUT_DOWN); + createJournalViewMenuItem.setAccelerator(newJournalKeyCombo); + KometPreferences journalPreferences = appPreferences.node(JOURNALS); + } + } + + private void restoreTab(KometPreferences windowPreferences, String tabPreferenceNodeName, ObservableViewNoOverride windowView, Consumer nodeConsumer) { + LOG.info("Restoring from: " + tabPreferenceNodeName); + KometPreferences itemPreferences = windowPreferences.node(KOMET_NODES + tabPreferenceNodeName); + itemPreferences.get(WindowComponent.WindowComponentKeys.FACTORY_CLASS).ifPresent(factoryClassName -> { + try { + Class objectClass = Class.forName(factoryClassName); + Class annotationClass = Reconstructor.class; + Object[] parameters = new Object[]{windowView, itemPreferences}; + WindowComponent windowComponent = (WindowComponent) Encodable.decode(objectClass, annotationClass, parameters); + nodeConsumer.accept(windowComponent.getNode()); + + } catch (Exception e) { + AlertStreams.getRoot().dispatch(AlertObject.makeError(e)); + } + }); + } + + private static ImmutableList makeDefaultLeftTabs(ObservableViewNoOverride windowView) { + GraphNavigatorNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory(); + KometNode navigatorNode1 = navigatorNodeFactory.create(windowView, + ActivityStreams.NAVIGATION, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab navigatorNode1Tab = new DetachableTab(navigatorNode1); + + + PatternNavigatorFactory patternNavigatorNodeFactory = new PatternNavigatorFactory(); + + KometNode patternNavigatorNode2 = patternNavigatorNodeFactory.create(windowView, + ActivityStreams.NAVIGATION, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + + DetachableTab patternNavigatorNode1Tab = new DetachableTab(patternNavigatorNode2); + + return Lists.immutable.of(navigatorNode1Tab, patternNavigatorNode1Tab); + } + + private static ImmutableList makeDefaultCenterTabs(ObservableViewNoOverride windowView) { + DetailsNodeFactory detailsNodeFactory = new DetailsNodeFactory(); + KometNode detailsNode1 = detailsNodeFactory.create(windowView, + ActivityStreams.NAVIGATION, ActivityStreamOption.SUBSCRIBE.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + + DetachableTab detailsNode1Tab = new DetachableTab(detailsNode1); + // TODO: setting up tab graphic, title, and tooltip needs to be standardized by the factory... + detailsNode1Tab.textProperty().bind(detailsNode1.getTitle()); + detailsNode1Tab.tooltipProperty().setValue(detailsNode1.makeToolTip()); + + KometNode detailsNode2 = detailsNodeFactory.create(windowView, + ActivityStreams.SEARCH, ActivityStreamOption.SUBSCRIBE.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab detailsNode2Tab = new DetachableTab(detailsNode2); + + KometNode detailsNode3 = detailsNodeFactory.create(windowView, + ActivityStreams.UNLINKED, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab detailsNode3Tab = new DetachableTab(detailsNode3); + + ListNodeFactory listNodeFactory = new ListNodeFactory(); + KometNode listNode = listNodeFactory.create(windowView, + ActivityStreams.LIST, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab listNodeNodeTab = new DetachableTab(listNode); + + TableNodeFactory tableNodeFactory = new TableNodeFactory(); + KometNode tableNode = tableNodeFactory.create(windowView, + ActivityStreams.UNLINKED, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab tableNodeTab = new DetachableTab(tableNode); + + return Lists.immutable.of(detailsNode1Tab, detailsNode2Tab, detailsNode3Tab, listNodeNodeTab, tableNodeTab); + } + + private static ImmutableList makeDefaultRightTabs(ObservableViewNoOverride windowView) { + SearchNodeFactory searchNodeFactory = new SearchNodeFactory(); + KometNode searchNode = searchNodeFactory.create(windowView, + ActivityStreams.SEARCH, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab newSearchTab = new DetachableTab(searchNode); + + ProgressNodeFactory progressNodeFactory = new ProgressNodeFactory(); + KometNode kometNode = progressNodeFactory.create(windowView, + null, null, AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab progressTab = new DetachableTab(kometNode); + + CompletionNodeFactory completionNodeFactory = new CompletionNodeFactory(); + KometNode completionNode = completionNodeFactory.create(windowView, + null, null, AlertStreams.ROOT_ALERT_STREAM_KEY); + DetachableTab completionTab = new DetachableTab(completionNode); + + return Lists.immutable.of(newSearchTab, progressTab, completionTab); + } + +} diff --git a/application/src/main/java/dev/ikm/komet/app/AppInterface.java b/application/src/main/java/dev/ikm/komet/app/AppInterface.java index 130ba6f32..6f6a21b8f 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppInterface.java +++ b/application/src/main/java/dev/ikm/komet/app/AppInterface.java @@ -1,7 +1,11 @@ package dev.ikm.komet.app; +import com.jpro.webapi.WebAPI; import dev.ikm.komet.kview.mvvm.model.GitHubPreferencesDao; import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController; +import javafx.scene.image.Image; +import javafx.stage.Stage; +import one.jpro.platform.auth.core.authentication.User; /** * Contains the shared elements between App and WebApp. @@ -9,6 +13,17 @@ */ public interface AppInterface { + /** + * Returns the default App icon. + * + * @return the {@link Image} representing the app icon + */ + Image getAppIcon(); + + AppMenu getAppMenu(); + + WebAPI getWebAPI(); + /** * Returns the controller for the landing page. * @@ -22,4 +37,19 @@ public interface AppInterface { * @return the {@link GitHubPreferencesDao} instance */ GitHubPreferencesDao getGitHubPreferencesDao(); + + /** + * Quits the application. + */ + void quit(); + + void launchLandingPage(Stage stage, User user); + + void showAboutDialog(); + + void openImport(Stage stage); + + void openExport(Stage stage); + + Stage getPrimaryStage(); } diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java new file mode 100644 index 000000000..fbd1b1f13 --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -0,0 +1,72 @@ +package dev.ikm.komet.app; + +import javafx.application.Platform; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyCodeCombination; +import javafx.scene.input.KeyCombination; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; + +import static dev.ikm.komet.app.WebApp.IS_BROWSER; + +public class AppMenu { + + private final AppInterface app; + + public AppMenu(AppInterface app) { + this.app = app; + } + + void generateMsWindowsMenu(BorderPane kometRoot, Stage stage) { + MenuBar menuBar = new MenuBar(); + Menu fileMenu = new Menu("File"); + + MenuItem about = new MenuItem("About"); + about.setOnAction(_ -> app.showAboutDialog()); + fileMenu.getItems().add(about); + + // Importing data + MenuItem importMenuItem = new MenuItem("Import Dataset"); + importMenuItem.setOnAction(actionEvent -> app.openImport(stage)); + fileMenu.getItems().add(importMenuItem); + + // Exporting data + MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset"); + exportDatasetMenuItem.setOnAction(actionEvent -> app.openExport(stage)); + fileMenu.getItems().add(exportDatasetMenuItem); + + MenuItem menuItemQuit = new MenuItem("Quit"); + KeyCombination quitKeyCombo = new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN); + menuItemQuit.setOnAction(actionEvent -> app.quit()); + menuItemQuit.setAccelerator(quitKeyCombo); + fileMenu.getItems().add(menuItemQuit); + + Menu editMenu = new Menu("Edit"); + MenuItem landingPage = new MenuItem("Landing Page"); + KeyCombination landingPageKeyCombo = new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN); + landingPage.setOnAction(actionEvent -> app.launchLandingPage(app.getPrimaryStage(), null /* userProperty.get()*/)); + landingPage.setAccelerator(landingPageKeyCombo); + landingPage.setDisable(IS_BROWSER); + editMenu.getItems().add(landingPage); + + Menu windowMenu = new Menu("Window"); + MenuItem minimizeWindow = new MenuItem("Minimize"); + KeyCombination minimizeKeyCombo = new KeyCodeCombination(KeyCode.M, KeyCombination.CONTROL_DOWN); + minimizeWindow.setOnAction(event -> { + Stage obj = (Stage) kometRoot.getScene().getWindow(); + obj.setIconified(true); + }); + minimizeWindow.setAccelerator(minimizeKeyCombo); + minimizeWindow.setDisable(IS_BROWSER); + windowMenu.getItems().add(minimizeWindow); + + menuBar.getMenus().add(fileMenu); + menuBar.getMenus().add(editMenu); + menuBar.getMenus().add(windowMenu); + //hBox.getChildren().add(menuBar); + Platform.runLater(() -> kometRoot.setTop(menuBar)); + } +} diff --git a/application/src/main/java/dev/ikm/komet/app/WebApp.java b/application/src/main/java/dev/ikm/komet/app/WebApp.java index 1f62ff290..907ae5632 100644 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ b/application/src/main/java/dev/ikm/komet/app/WebApp.java @@ -168,26 +168,46 @@ public class WebApp extends Application implements AppInterface { public static final SimpleObjectProperty userProperty = new SimpleObjectProperty<>(); private static Stage primaryStage; - private static Stage classicKometStage; private static long windowCount = 1; private static KometPreferencesStage kometPreferencesStage; private static WebAPI webAPI; - private static final boolean IS_BROWSER = WebAPI.isBrowser(); - private static final boolean IS_DESKTOP = !IS_BROWSER && PlatformUtils.isDesktop(); - private static final boolean IS_MAC = !IS_BROWSER && PlatformUtils.isMac(); - private static final boolean IS_MAC_AND_NOT_TESTFX_TEST = IS_MAC && !isTestFXTest(); + static final boolean IS_BROWSER = WebAPI.isBrowser(); + static final boolean IS_DESKTOP = !IS_BROWSER && PlatformUtils.isDesktop(); + static final boolean IS_MAC = !IS_BROWSER && PlatformUtils.isMac(); + static final boolean IS_MAC_AND_NOT_TESTFX_TEST = IS_MAC && !isTestFXTest(); private final StackPane rootPane = createRootPane(); private Image appIcon; private LandingPageController landingPageController; private EvtBus kViewEventBus; AppGithub appGithub; + AppClassicKomet appClassicKomet; + AppMenu appMenu; + @Override + public AppMenu getAppMenu() { + return appMenu; + } + + @Override + public WebAPI getWebAPI() { + return webAPI; + } + + @Override + public Image getAppIcon() { + return appIcon; + } + @Override + public Stage getPrimaryStage() { + return primaryStage; + } @Override public LandingPageController getLandingPageController() { return landingPageController; } + @Override public GitHubPreferencesDao getGitHubPreferencesDao() { return gitHubPreferencesDao; @@ -261,73 +281,6 @@ private static void createNewStage() { stage.show(); } - private static ImmutableList makeDefaultLeftTabs(ObservableViewNoOverride windowView) { - GraphNavigatorNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory(); - KometNode navigatorNode1 = navigatorNodeFactory.create(windowView, - ActivityStreams.NAVIGATION, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab navigatorNode1Tab = new DetachableTab(navigatorNode1); - - - PatternNavigatorFactory patternNavigatorNodeFactory = new PatternNavigatorFactory(); - - KometNode patternNavigatorNode2 = patternNavigatorNodeFactory.create(windowView, - ActivityStreams.NAVIGATION, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - - DetachableTab patternNavigatorNode1Tab = new DetachableTab(patternNavigatorNode2); - - return Lists.immutable.of(navigatorNode1Tab, patternNavigatorNode1Tab); - } - - private static ImmutableList makeDefaultCenterTabs(ObservableViewNoOverride windowView) { - DetailsNodeFactory detailsNodeFactory = new DetailsNodeFactory(); - KometNode detailsNode1 = detailsNodeFactory.create(windowView, - ActivityStreams.NAVIGATION, ActivityStreamOption.SUBSCRIBE.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - - DetachableTab detailsNode1Tab = new DetachableTab(detailsNode1); - // TODO: setting up tab graphic, title, and tooltip needs to be standardized by the factory... - detailsNode1Tab.textProperty().bind(detailsNode1.getTitle()); - detailsNode1Tab.tooltipProperty().setValue(detailsNode1.makeToolTip()); - - KometNode detailsNode2 = detailsNodeFactory.create(windowView, - ActivityStreams.SEARCH, ActivityStreamOption.SUBSCRIBE.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab detailsNode2Tab = new DetachableTab(detailsNode2); - - KometNode detailsNode3 = detailsNodeFactory.create(windowView, - ActivityStreams.UNLINKED, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab detailsNode3Tab = new DetachableTab(detailsNode3); - - ListNodeFactory listNodeFactory = new ListNodeFactory(); - KometNode listNode = listNodeFactory.create(windowView, - ActivityStreams.LIST, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab listNodeNodeTab = new DetachableTab(listNode); - - TableNodeFactory tableNodeFactory = new TableNodeFactory(); - KometNode tableNode = tableNodeFactory.create(windowView, - ActivityStreams.UNLINKED, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab tableNodeTab = new DetachableTab(tableNode); - - return Lists.immutable.of(detailsNode1Tab, detailsNode2Tab, detailsNode3Tab, listNodeNodeTab, tableNodeTab); - } - - private static ImmutableList makeDefaultRightTabs(ObservableViewNoOverride windowView) { - SearchNodeFactory searchNodeFactory = new SearchNodeFactory(); - KometNode searchNode = searchNodeFactory.create(windowView, - ActivityStreams.SEARCH, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab newSearchTab = new DetachableTab(searchNode); - - ProgressNodeFactory progressNodeFactory = new ProgressNodeFactory(); - KometNode kometNode = progressNodeFactory.create(windowView, - null, null, AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab progressTab = new DetachableTab(kometNode); - - CompletionNodeFactory completionNodeFactory = new CompletionNodeFactory(); - KometNode completionNode = completionNodeFactory.create(windowView, - null, null, AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab completionTab = new DetachableTab(completionNode); - - return Lists.immutable.of(newSearchTab, progressTab, completionTab); - } - @Override public void init() throws Exception { LOG.info("Starting Komet"); @@ -399,6 +352,8 @@ public void init() throws Exception { @Override public void start(Stage stage) { appGithub = new AppGithub(this); + appClassicKomet = new AppClassicKomet(this); + appMenu = new AppMenu(this); try { WebApp.primaryStage = stage; @@ -620,7 +575,7 @@ private Menu createViewMenu() { classicKometMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN)); classicKometMenuItem.setOnAction(actionEvent -> { try { - launchClassicKomet(); + appClassicKomet.launchClassicKomet(); } catch (IOException | BackingStoreException e) { throw new RuntimeException(e); } @@ -705,7 +660,7 @@ private void addEventFilters(Stage stage) { }); } - private void launchLandingPage(Stage stage, User user) { + public void launchLandingPage(Stage stage, User user) { try { rootPane.getChildren().clear(); // Clear the root pane before adding new content @@ -773,7 +728,7 @@ private void launchJournalViewPage(PrefX journalWindowSettings) { journalStage.setScene(sourceScene); if (!IS_MAC) { - generateMsWindowsMenu(journalBorderPane, journalStage); + appMenu.generateMsWindowsMenu(journalBorderPane, journalStage); } // load journal specific window settings @@ -949,93 +904,7 @@ private void appStateChangeListener(ObservableValue observab } } - private void launchClassicKomet() throws IOException, BackingStoreException { - if (IS_DESKTOP) { - // If already launched bring to the front - if (classicKometStage != null && classicKometStage.isShowing()) { - classicKometStage.show(); - classicKometStage.toFront(); - return; - } - } - - classicKometStage = new Stage(); - classicKometStage.getIcons().setAll(appIcon); - - //Starting up preferences and getting configurations - Preferences.start(); - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - boolean appInitialized = appPreferences.getBoolean(AppKeys.APP_INITIALIZED, false); - if (appInitialized) { - LOG.info("Restoring configuration preferences."); - } else { - LOG.info("Creating new configuration preferences."); - } - - MainWindowRecord mainWindowRecord = MainWindowRecord.make(); - - BorderPane kometRoot = mainWindowRecord.root(); - KometStageController controller = mainWindowRecord.controller(); - - //Loading/setting the Komet screen - Scene kometScene = new Scene(kometRoot, 1800, 1024); - addStylesheets(kometScene, KOMET_CSS); - - // if NOT on macOS - if (!IS_MAC) { - generateMsWindowsMenu(kometRoot, classicKometStage); - } - - classicKometStage.setScene(kometScene); - - KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - boolean mainWindowInitialized = windowPreferences.getBoolean(KometStageController.WindowKeys.WINDOW_INITIALIZED, false); - controller.setup(windowPreferences, classicKometStage); - classicKometStage.setTitle("Komet"); - - if (!mainWindowInitialized) { - controller.setLeftTabs(makeDefaultLeftTabs(controller.windowView()), 0); - controller.setCenterTabs(makeDefaultCenterTabs(controller.windowView()), 0); - controller.setRightTabs(makeDefaultRightTabs(controller.windowView()), 1); - windowPreferences.putBoolean(KometStageController.WindowKeys.WINDOW_INITIALIZED, true); - appPreferences.putBoolean(AppKeys.APP_INITIALIZED, true); - } else { - // Restore nodes from preferences. - windowPreferences.get(LEFT_TAB_PREFERENCES).ifPresent(leftTabPreferencesName -> - restoreTab(windowPreferences, leftTabPreferencesName, controller.windowView(), - controller::leftBorderPaneSetCenter)); - windowPreferences.get(CENTER_TAB_PREFERENCES).ifPresent(centerTabPreferencesName -> - restoreTab(windowPreferences, centerTabPreferencesName, controller.windowView(), - controller::centerBorderPaneSetCenter)); - windowPreferences.get(RIGHT_TAB_PREFERENCES).ifPresent(rightTabPreferencesName -> - restoreTab(windowPreferences, rightTabPreferencesName, controller.windowView(), - controller::rightBorderPaneSetCenter)); - } - //Setting X and Y coordinates for location of the Komet stage - classicKometStage.setX(controller.windowSettings().xLocationProperty().get()); - classicKometStage.setY(controller.windowSettings().yLocationProperty().get()); - classicKometStage.setHeight(controller.windowSettings().heightProperty().get()); - classicKometStage.setWidth(controller.windowSettings().widthProperty().get()); - classicKometStage.show(); - - if (IS_BROWSER) { - webAPI.openStageAsTab(classicKometStage); - } - - WebApp.kometPreferencesStage = new KometPreferencesStage(controller.windowView().makeOverridableViewProperties()); - - windowPreferences.sync(); - appPreferences.sync(); - - if (createJournalViewMenuItem != null) { - createJournalViewMenuItem.setDisable(false); - KeyCombination newJournalKeyCombo = new KeyCodeCombination(KeyCode.J, KeyCombination.SHORTCUT_DOWN); - createJournalViewMenuItem.setAccelerator(newJournalKeyCombo); - KometPreferences journalPreferences = appPreferences.node(JOURNALS); - } - } - - private void openImport(Window owner) { + public void openImport(Stage owner) { KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); WindowSettings windowSettings = new WindowSettings(windowPreferences); @@ -1054,7 +923,7 @@ private void openImport(Window owner) { importStage.show(); } - private void openExport(Window owner) { + public void openExport(Stage owner) { KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); WindowSettings windowSettings = new WindowSettings(windowPreferences); @@ -1190,62 +1059,12 @@ private CompletableFuture showRepositoryInfoDialog(Map showAboutDialog()); - fileMenu.getItems().add(about); - - // Importing data - MenuItem importMenuItem = new MenuItem("Import Dataset"); - importMenuItem.setOnAction(actionEvent -> openImport(stage)); - fileMenu.getItems().add(importMenuItem); - - // Exporting data - MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset"); - exportDatasetMenuItem.setOnAction(actionEvent -> openExport(stage)); - fileMenu.getItems().add(exportDatasetMenuItem); - - MenuItem menuItemQuit = new MenuItem("Quit"); - KeyCombination quitKeyCombo = new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN); - menuItemQuit.setOnAction(actionEvent -> quit()); - menuItemQuit.setAccelerator(quitKeyCombo); - fileMenu.getItems().add(menuItemQuit); - - Menu editMenu = new Menu("Edit"); - MenuItem landingPage = new MenuItem("Landing Page"); - KeyCombination landingPageKeyCombo = new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN); - landingPage.setOnAction(actionEvent -> launchLandingPage(primaryStage, userProperty.get())); - landingPage.setAccelerator(landingPageKeyCombo); - landingPage.setDisable(IS_BROWSER); - editMenu.getItems().add(landingPage); - - Menu windowMenu = new Menu("Window"); - MenuItem minimizeWindow = new MenuItem("Minimize"); - KeyCombination minimizeKeyCombo = new KeyCodeCombination(KeyCode.M, KeyCombination.CONTROL_DOWN); - minimizeWindow.setOnAction(event -> { - Stage obj = (Stage) kometRoot.getScene().getWindow(); - obj.setIconified(true); - }); - minimizeWindow.setAccelerator(minimizeKeyCombo); - minimizeWindow.setDisable(IS_BROWSER); - windowMenu.getItems().add(minimizeWindow); - - menuBar.getMenus().add(fileMenu); - menuBar.getMenus().add(editMenu); - menuBar.getMenus().add(windowMenu); - //hBox.getChildren().add(menuBar); - Platform.runLater(() -> kometRoot.setTop(menuBar)); - } - - private void showAboutDialog() { + public void showAboutDialog() { AboutDialog aboutDialog = new AboutDialog(); aboutDialog.showAndWait(); } - private void quit() { + public void quit() { saveJournalWindowsToPreferences(); PrimitiveData.stop(); Preferences.stop(); @@ -1351,7 +1170,7 @@ private MenuItem createClassicKometMenuItem() { KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN); classicKometMenuItem.setOnAction(actionEvent -> { try { - launchClassicKomet(); + appClassicKomet.launchClassicKomet(); } catch (IOException e) { throw new RuntimeException(e); } catch (BackingStoreException e) { From 4505729fe4b3e5a8396a61333fa434c3fe8f3af6 Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Thu, 7 Aug 2025 20:55:21 +0200 Subject: [PATCH 03/10] Refactor App and WebApp to improve modularity by adding AppClassicKomet and AppMenu getters, and streamline menu creation in AppMenu. --- .../src/main/java/dev/ikm/komet/app/App.java | 215 ++--------------- .../java/dev/ikm/komet/app/AppGithub.java | 128 +++++++++- .../java/dev/ikm/komet/app/AppInterface.java | 11 +- .../main/java/dev/ikm/komet/app/AppMenu.java | 81 ++++++- .../main/java/dev/ikm/komet/app/WebApp.java | 219 +----------------- 5 files changed, 240 insertions(+), 414 deletions(-) diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 9f9824ce3..148b6e83e 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -160,11 +160,21 @@ public class App extends Application implements AppInterface { AppClassicKomet appClassicKomet; AppMenu appMenu; + @Override + public AppGithub getAppGithub() { + return appGithub; + } + @Override public AppMenu getAppMenu() { return appMenu; } + @Override + public AppClassicKomet getAppClassicKomet() { + return appClassicKomet; + } + @Override public WebAPI getWebAPI() { return null; @@ -319,6 +329,7 @@ public void init() { @Override public void start(Stage stage) { + /* appGithub = new AppGithub(this); appClassicKomet = new AppClassicKomet(this); appMenu = new AppMenu(this); @@ -347,6 +358,7 @@ public void start(Stage stage) { // File Menu Menu fileMenu = new Menu("File"); + MenuItem importDatasetMenuItem = new MenuItem("Import Dataset"); importDatasetMenuItem.setOnAction(actionEvent -> openImport(primaryStage)); @@ -431,7 +443,7 @@ public void start(Stage stage) { } catch (IOException e) { LOG.error(e.getLocalizedMessage(), e); Platform.exit(); - } + }*/ } public void launchLandingPage(Stage stage, User user) { @@ -453,7 +465,7 @@ public void launchLandingPage(Stage stage, User user) { BorderPane landingPageBorderPane = landingPageLoader.load(); // if NOT on Mac OS if (System.getProperty("os.name") != null && !System.getProperty("os.name").toLowerCase().startsWith(OS_NAME_MAC)) { - createMenuOptions(landingPageBorderPane); + appMenu.createMenuOptions(landingPageBorderPane); } landingPageController = landingPageLoader.getController(); landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); @@ -741,140 +753,6 @@ public void openExport(Stage stage) { exportStage.show(); } - /** - * Displays information about the current Git repository. - *

- * This method checks if a Git repository exists and displays basic information about it. - * If no repository exists or is not properly configured, the user will be prompted to - * enter GitHub preferences before proceeding. Upon successful connection to GitHub, - * repository information will be fetched and displayed in a dialog. - *

- * The method performs the following operations: - *

    - *
  1. Verifies that the data store root is available
  2. - *
  3. Checks if a Git repository exists in the changeset folder
  4. - *
  5. If no repository exists, prompts for GitHub preferences and initiates connection
  6. - *
  7. Fetches and displays repository information
  8. - *
- */ - private void infoAction() { - Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); - if (optionalDataStoreRoot.isEmpty()) { - LOG.error("ServiceKeys.DATA_STORE_ROOT not provided."); - return; - } - - final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); - final File gitDir = new File(changeSetFolder, ".git"); - - if (gitDir.exists()) { - fetchAndShowRepositoryInfo(changeSetFolder); - } else { - // Prompt for preferences before proceeding - appGithub.promptForGitHubPrefs().thenCompose(confirmed -> { - if (confirmed) { - // Preferences entered successfully, now run the GitTask - return appGithub.createAndRunGitTask(CONNECT, changeSetFolder); - } else { - return CompletableFuture.completedFuture(false); - } - }).thenAccept(confirmed -> { - if (confirmed) { - fetchAndShowRepositoryInfo(changeSetFolder); - } - }); - } - } - - /** - * Fetches repository information and displays it in a dialog. - *

- * This method asynchronously retrieves information about the Git repository - * located in the specified folder using an {@code InfoTask}, then displays - * the results in a dialog. The operation is performed on a background thread - * to avoid blocking the UI. - * - * @param changeSetFolder The repository folder to fetch information from - */ - private void fetchAndShowRepositoryInfo(File changeSetFolder) { - CompletableFuture.supplyAsync(() -> { - try { - InfoTask task = new InfoTask(changeSetFolder.toPath()); - return task.call(); - } catch (Exception ex) { - throw new RuntimeException("Failed to fetch repository information", ex); - } - }, TinkExecutor.threadPool()) - .thenCompose(repoInfo -> showRepositoryInfoDialog(repoInfo) - .thenAccept(confirmed -> { - if (confirmed) { - LOG.info("User closed the repository info dialog"); - } - })); - } - - /** - * Displays the repository information dialog. - *

- * This method creates and displays a dialog showing Git repository information - * including URL, username, email, and status. The dialog is displayed using a - * glass pane overlay on top of the landing page. - *

- * The method returns a CompletableFuture that will be completed when the user - * closes the dialog. - * - * @param repoInfo Map containing repository information with keys defined in {@code GitPropertyName} - * @return A CompletableFuture that completes with {@code true} when the user closes the dialog - */ - private CompletableFuture showRepositoryInfoDialog(Map repoInfo) { - // Create a CompletableFuture that will be completed when the user makes a choice - CompletableFuture future = new CompletableFuture<>(); - - // Show dialog on JavaFX thread - runOnFxThread(() -> { - GlassPane glassPane = new GlassPane(landingPageController.getRoot()); - - final JFXNode githubInfoNode = FXMLMvvmLoader - .make(GitHubInfoController.class.getResource("github-info.fxml")); - final Pane dialogPane = githubInfoNode.node(); - final GitHubInfoController controller = githubInfoNode.controller(); - - controller.getGitUrlTextField().setText(repoInfo.get(GIT_URL)); - controller.getGitUsernameTextField().setText(repoInfo.get(GIT_USERNAME)); - controller.getGitEmailTextField().setText(repoInfo.get(GIT_EMAIL)); - controller.getStatusTextArea().setText(repoInfo.get(GIT_STATUS)); - - controller.getCloseButton().setOnAction(_ -> { - glassPane.removeContent(dialogPane); - glassPane.hide(); - future.complete(true); // Complete with true on close - }); - - glassPane.addContent(dialogPane); - glassPane.show(); - }); - - return future; - } - - public void showAboutDialog() { - AboutDialog aboutDialog = new AboutDialog(); - aboutDialog.showAndWait(); - } - private Menu createExchangeMenu() { - Menu exchangeMenu = new Menu("Exchange"); - - MenuItem infoMenuItem = new MenuItem("Info"); - infoMenuItem.setOnAction(actionEvent -> infoAction()); - MenuItem pullMenuItem = new MenuItem("Pull"); - pullMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(PULL)); - MenuItem pushMenuItem = new MenuItem("Sync"); - pushMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(SYNC)); - - exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); - return exchangeMenu; - } - public void quit() { saveJournalWindowsToPreferences(); PrimitiveData.stop(); @@ -882,71 +760,6 @@ public void quit() { Platform.exit(); } - /** - * Create the menu options for the landing page. - * - * @param landingPageRoot The root pane of the landing page. - */ - public void createMenuOptions(final BorderPane landingPageRoot) { - MenuBar menuBar = new MenuBar(); - - Menu fileMenu = new Menu("File"); - MenuItem about = new MenuItem("About"); - about.setOnAction(actionEvent -> showAboutDialog()); - fileMenu.getItems().add(about); - - MenuItem menuItemQuit = new MenuItem("Quit"); - KeyCombination quitKeyCombo = new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN); - menuItemQuit.setOnAction(actionEvent -> quit()); - menuItemQuit.setAccelerator(quitKeyCombo); - fileMenu.getItems().add(menuItemQuit); - - Menu viewMenu = new Menu("View"); - MenuItem classicKometMenuItem = createClassicKometMenuItem(); - MenuItem resourceUsageMenuItem = createResourceUsageItem(); - viewMenu.getItems().addAll(classicKometMenuItem, resourceUsageMenuItem); - - Menu windowMenu = new Menu("Window"); - MenuItem minimizeWindow = new MenuItem("Minimize"); - KeyCombination minimizeKeyCombo = new KeyCodeCombination(KeyCode.M, KeyCombination.CONTROL_DOWN); - minimizeWindow.setOnAction(event -> { - Stage obj = (Stage) landingPageRoot.getScene().getWindow(); - obj.setIconified(true); - }); - minimizeWindow.setAccelerator(minimizeKeyCombo); - windowMenu.getItems().add(minimizeWindow); - - // Exchange Menu - Menu exchangeMenu = createExchangeMenu(); - - menuBar.getMenus().add(fileMenu); - menuBar.getMenus().add(viewMenu); - menuBar.getMenus().add(windowMenu); - menuBar.getMenus().add(exchangeMenu); - landingPageRoot.setTop(menuBar); - } - - /** - * Create the menu item for launching the classic Komet. - * - * @return The menu item for launching the classic Komet. - */ - private MenuItem createClassicKometMenuItem() { - MenuItem classicKometMenuItem = new MenuItem("Classic Komet"); - KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN); - classicKometMenuItem.setOnAction(actionEvent -> { - try { - appClassicKomet.launchClassicKomet(); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (BackingStoreException e) { - throw new RuntimeException(e); - } - }); - classicKometMenuItem.setAccelerator(classicKometKeyCombo); - return classicKometMenuItem; - } - /** * Create a Resource Usage overlay to display metrics * diff --git a/application/src/main/java/dev/ikm/komet/app/AppGithub.java b/application/src/main/java/dev/ikm/komet/app/AppGithub.java index cf179ce98..a6ab07945 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppGithub.java +++ b/application/src/main/java/dev/ikm/komet/app/AppGithub.java @@ -2,11 +2,11 @@ import dev.ikm.komet.framework.progress.ProgressHelper; import dev.ikm.komet.kview.controls.GlassPane; -import dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitHubPreferencesController; -import dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask; +import dev.ikm.komet.kview.mvvm.view.changeset.exchange.*; import dev.ikm.komet.kview.mvvm.viewmodel.GitHubPreferencesViewModel; import dev.ikm.tinkar.common.service.ServiceKeys; import dev.ikm.tinkar.common.service.ServiceProperties; +import dev.ikm.tinkar.common.service.TinkExecutor; import javafx.scene.control.Hyperlink; import javafx.scene.layout.Pane; import org.carlfx.cognitive.loader.FXMLMvvmLoader; @@ -15,12 +15,15 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.prefs.BackingStoreException; import static dev.ikm.komet.framework.events.FrameworkTopics.LANDING_PAGE_TOPIC; import static dev.ikm.komet.kview.fxutils.FXUtils.runOnFxThread; +import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitPropertyName.*; +import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitPropertyName.GIT_STATUS; import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.CONNECT; public class AppGithub { @@ -285,4 +288,125 @@ void disconnectFromGithub() { // Update the UI state gotoGitHubDisconnectedState(); } + + + + /** + * Displays information about the current Git repository. + *

+ * This method checks if a Git repository exists and displays basic information about it. + * If no repository exists or is not properly configured, the user will be prompted to + * enter GitHub preferences before proceeding. Upon successful connection to GitHub, + * repository information will be fetched and displayed in a dialog. + *

+ * The method performs the following operations: + *

    + *
  1. Verifies that the data store root is available
  2. + *
  3. Checks if a Git repository exists in the changeset folder
  4. + *
  5. If no repository exists, prompts for GitHub preferences and initiates connection
  6. + *
  7. Fetches and displays repository information
  8. + *
+ */ + void infoAction() { + Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); + if (optionalDataStoreRoot.isEmpty()) { + LOG.error("ServiceKeys.DATA_STORE_ROOT not provided."); + return; + } + + final File changeSetFolder = new File(optionalDataStoreRoot.get(), CHANGESETS_DIR); + final File gitDir = new File(changeSetFolder, ".git"); + + if (gitDir.exists()) { + fetchAndShowRepositoryInfo(changeSetFolder); + } else { + // Prompt for preferences before proceeding + promptForGitHubPrefs().thenCompose(confirmed -> { + if (confirmed) { + // Preferences entered successfully, now run the GitTask + return createAndRunGitTask(CONNECT, changeSetFolder); + } else { + return CompletableFuture.completedFuture(false); + } + }).thenAccept(confirmed -> { + if (confirmed) { + fetchAndShowRepositoryInfo(changeSetFolder); + } + }); + } + } + + /** + * Fetches repository information and displays it in a dialog. + *

+ * This method asynchronously retrieves information about the Git repository + * located in the specified folder using an {@code InfoTask}, then displays + * the results in a dialog. The operation is performed on a background thread + * to avoid blocking the UI. + * + * @param changeSetFolder The repository folder to fetch information from + */ + private void fetchAndShowRepositoryInfo(File changeSetFolder) { + CompletableFuture.supplyAsync(() -> { + try { + InfoTask task = new InfoTask(changeSetFolder.toPath()); + return task.call(); + } catch (Exception ex) { + throw new RuntimeException("Failed to fetch repository information", ex); + } + }, TinkExecutor.threadPool()) + .thenCompose(repoInfo -> showRepositoryInfoDialog(repoInfo) + .thenAccept(confirmed -> { + if (confirmed) { + LOG.info("User closed the repository info dialog"); + } + })); + } + + /** + * Displays the repository information dialog. + *

+ * This method creates and displays a dialog showing Git repository information + * including URL, username, email, and status. The dialog is displayed using a + * glass pane overlay on top of the landing page. + *

+ * The method returns a CompletableFuture that will be completed when the user + * closes the dialog. + * + * @param repoInfo Map containing repository information with keys defined in {@code GitPropertyName} + * @return A CompletableFuture that completes with {@code true} when the user closes the dialog + */ + private CompletableFuture showRepositoryInfoDialog(Map repoInfo) { + // Create a CompletableFuture that will be completed when the user makes a choice + CompletableFuture future = new CompletableFuture<>(); + + // Show dialog on JavaFX thread + runOnFxThread(() -> { + GlassPane glassPane = new GlassPane(app.getLandingPageController().getRoot()); + + final JFXNode githubInfoNode = FXMLMvvmLoader + .make(GitHubInfoController.class.getResource("github-info.fxml")); + final Pane dialogPane = githubInfoNode.node(); + final GitHubInfoController controller = githubInfoNode.controller(); + + controller.getGitUrlTextField().setText(repoInfo.get(GIT_URL)); + controller.getGitUsernameTextField().setText(repoInfo.get(GIT_USERNAME)); + controller.getGitEmailTextField().setText(repoInfo.get(GIT_EMAIL)); + controller.getStatusTextArea().setText(repoInfo.get(GIT_STATUS)); + + controller.getCloseButton().setOnAction(_ -> { + glassPane.removeContent(dialogPane); + glassPane.hide(); + future.complete(true); // Complete with true on close + }); + + glassPane.addContent(dialogPane); + glassPane.show(); + }); + + return future; + } + + + } diff --git a/application/src/main/java/dev/ikm/komet/app/AppInterface.java b/application/src/main/java/dev/ikm/komet/app/AppInterface.java index 6f6a21b8f..a575fb768 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppInterface.java +++ b/application/src/main/java/dev/ikm/komet/app/AppInterface.java @@ -13,6 +13,13 @@ */ public interface AppInterface { + + AppMenu getAppMenu(); + + AppGithub getAppGithub(); + + AppClassicKomet getAppClassicKomet(); + /** * Returns the default App icon. * @@ -20,8 +27,6 @@ public interface AppInterface { */ Image getAppIcon(); - AppMenu getAppMenu(); - WebAPI getWebAPI(); /** @@ -45,7 +50,7 @@ public interface AppInterface { void launchLandingPage(Stage stage, User user); - void showAboutDialog(); + //void showAboutDialog(); void openImport(Stage stage); diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java index fbd1b1f13..9d2d088f6 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppMenu.java +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -1,5 +1,6 @@ package dev.ikm.komet.app; +import dev.ikm.komet.app.aboutdialog.AboutDialog; import javafx.application.Platform; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; @@ -10,7 +11,12 @@ import javafx.scene.layout.BorderPane; import javafx.stage.Stage; +import java.io.IOException; +import java.util.prefs.BackingStoreException; + import static dev.ikm.komet.app.WebApp.IS_BROWSER; +import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.PULL; +import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.SYNC; public class AppMenu { @@ -25,7 +31,7 @@ void generateMsWindowsMenu(BorderPane kometRoot, Stage stage) { Menu fileMenu = new Menu("File"); MenuItem about = new MenuItem("About"); - about.setOnAction(_ -> app.showAboutDialog()); + about.setOnAction(_ -> showAboutDialog()); fileMenu.getItems().add(about); // Importing data @@ -69,4 +75,77 @@ void generateMsWindowsMenu(BorderPane kometRoot, Stage stage) { //hBox.getChildren().add(menuBar); Platform.runLater(() -> kometRoot.setTop(menuBar)); } + + Menu createExchangeMenu() { + Menu exchangeMenu = new Menu("Exchange"); + + MenuItem infoMenuItem = new MenuItem("Info"); + infoMenuItem.setOnAction(actionEvent -> app.getAppGithub().infoAction()); + MenuItem pullMenuItem = new MenuItem("Pull"); + pullMenuItem.setOnAction(actionEvent -> app.getAppGithub().executeGitTask(PULL)); + MenuItem pushMenuItem = new MenuItem("Sync"); + pushMenuItem.setOnAction(actionEvent -> app.getAppGithub().executeGitTask(SYNC)); + + exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); + return exchangeMenu; + } + + public void showAboutDialog() { + AboutDialog aboutDialog = new AboutDialog(); + aboutDialog.showAndWait(); + } + + public void createMenuOptions(BorderPane landingPageRoot) { + MenuBar menuBar = new MenuBar(); + + Menu fileMenu = new Menu("File"); + MenuItem about = new MenuItem("About"); + about.setOnAction(_ -> showAboutDialog()); + fileMenu.getItems().add(about); + + MenuItem menuItemQuit = new MenuItem("Quit"); + KeyCombination quitKeyCombo = new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN); + menuItemQuit.setOnAction(actionEvent -> app.quit()); + menuItemQuit.setAccelerator(quitKeyCombo); + fileMenu.getItems().add(menuItemQuit); + + Menu viewMenu = new Menu("View"); + MenuItem classicKometMenuItem = createClassicKometMenuItem(); + viewMenu.getItems().add(classicKometMenuItem); + + Menu windowMenu = new Menu("Window"); + MenuItem minimizeWindow = new MenuItem("Minimize"); + KeyCombination minimizeKeyCombo = new KeyCodeCombination(KeyCode.M, KeyCombination.CONTROL_DOWN); + minimizeWindow.setOnAction(event -> { + Stage obj = (Stage) landingPageRoot.getScene().getWindow(); + obj.setIconified(true); + }); + minimizeWindow.setAccelerator(minimizeKeyCombo); + minimizeWindow.setDisable(IS_BROWSER); + windowMenu.getItems().add(minimizeWindow); + + Menu exchangeMenu = createExchangeMenu(); + + menuBar.getMenus().add(fileMenu); + menuBar.getMenus().add(viewMenu); + menuBar.getMenus().add(windowMenu); + menuBar.getMenus().add(exchangeMenu); + landingPageRoot.setTop(menuBar); + } + + private MenuItem createClassicKometMenuItem() { + MenuItem classicKometMenuItem = new MenuItem("Classic Komet"); + KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN); + classicKometMenuItem.setOnAction(actionEvent -> { + try { + app.getAppClassicKomet().launchClassicKomet(); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (BackingStoreException e) { + throw new RuntimeException(e); + } + }); + classicKometMenuItem.setAccelerator(classicKometKeyCombo); + return classicKometMenuItem; + } } diff --git a/application/src/main/java/dev/ikm/komet/app/WebApp.java b/application/src/main/java/dev/ikm/komet/app/WebApp.java index 907ae5632..1533cc33c 100644 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ b/application/src/main/java/dev/ikm/komet/app/WebApp.java @@ -185,11 +185,21 @@ public class WebApp extends Application implements AppInterface { AppClassicKomet appClassicKomet; AppMenu appMenu; + @Override + public AppGithub getAppGithub() { + return appGithub; + } + @Override public AppMenu getAppMenu() { return appMenu; } + @Override + public AppClassicKomet getAppClassicKomet() { + return appClassicKomet; + } + @Override public WebAPI getWebAPI() { return webAPI; @@ -524,7 +534,7 @@ private void setupMenus() { } // Create and add the exchange menu to the menu bar - Menu exchangeMenu = createExchangeMenu(); + Menu exchangeMenu = appMenu.createExchangeMenu(); menuBar.getMenus().add(exchangeMenu); // Create and add the help menu to the menu bar @@ -596,19 +606,7 @@ private Menu createWindowMenuOnMacOS() { return windowMenu; } - private Menu createExchangeMenu() { - Menu exchangeMenu = new Menu("Exchange"); - - MenuItem infoMenuItem = new MenuItem("Info"); - infoMenuItem.setOnAction(actionEvent -> infoAction()); - MenuItem pullMenuItem = new MenuItem("Pull"); - pullMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(PULL)); - MenuItem pushMenuItem = new MenuItem("Sync"); - pushMenuItem.setOnAction(actionEvent -> appGithub.executeGitTask(SYNC)); - exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); - return exchangeMenu; - } private Menu createHelpMenu() { Menu helpMenu = new Menu("Help"); @@ -672,7 +670,7 @@ public void launchLandingPage(Stage stage, User user) { BorderPane landingPageBorderPane = landingPageLoader.load(); if (!IS_MAC) { - createMenuOptions(landingPageBorderPane); + appMenu.createMenuOptions(landingPageBorderPane); } landingPageController = landingPageLoader.getController(); @@ -942,128 +940,6 @@ public void openExport(Stage owner) { exportStage.show(); } - - /** - * Displays information about the current Git repository. - *

- * This method checks if a Git repository exists and displays basic information about it. - * If no repository exists or is not properly configured, the user will be prompted to - * enter GitHub preferences before proceeding. Upon successful connection to GitHub, - * repository information will be fetched and displayed in a dialog. - *

- * The method performs the following operations: - *

    - *
  1. Verifies that the data store root is available
  2. - *
  3. Checks if a Git repository exists in the changeset folder
  4. - *
  5. If no repository exists, prompts for GitHub preferences and initiates connection
  6. - *
  7. Fetches and displays repository information
  8. - *
- */ - private void infoAction() { - Optional optionalDataStoreRoot = ServiceProperties.get(ServiceKeys.DATA_STORE_ROOT); - if (optionalDataStoreRoot.isEmpty()) { - LOG.error("ServiceKeys.DATA_STORE_ROOT not provided."); - return; - } - - final File changeSetFolder = new File(optionalDataStoreRoot.get(), AppGithub.CHANGESETS_DIR); - final File gitDir = new File(changeSetFolder, ".git"); - - if (gitDir.exists()) { - fetchAndShowRepositoryInfo(changeSetFolder); - } else { - // Prompt for preferences before proceeding - appGithub.promptForGitHubPrefs().thenCompose(confirmed -> { - if (confirmed) { - // Preferences entered successfully, now run the GitTask - return appGithub.createAndRunGitTask(CONNECT, changeSetFolder); - } else { - return CompletableFuture.completedFuture(false); - } - }).thenAccept(confirmed -> { - if (confirmed) { - fetchAndShowRepositoryInfo(changeSetFolder); - } - }); - } - } - - /** - * Fetches repository information and displays it in a dialog. - *

- * This method asynchronously retrieves information about the Git repository - * located in the specified folder using an {@code InfoTask}, then displays - * the results in a dialog. The operation is performed on a background thread - * to avoid blocking the UI. - * - * @param changeSetFolder The repository folder to fetch information from - */ - private void fetchAndShowRepositoryInfo(File changeSetFolder) { - CompletableFuture.supplyAsync(() -> { - try { - InfoTask task = new InfoTask(changeSetFolder.toPath()); - return task.call(); - } catch (Exception ex) { - throw new RuntimeException("Failed to fetch repository information", ex); - } - }, TinkExecutor.threadPool()) - .thenCompose(repoInfo -> showRepositoryInfoDialog(repoInfo) - .thenAccept(confirmed -> { - if (confirmed) { - LOG.info("User closed the repository info dialog"); - } - })); - } - - /** - * Displays the repository information dialog. - *

- * This method creates and displays a dialog showing Git repository information - * including URL, username, email, and status. The dialog is displayed using a - * glass pane overlay on top of the landing page. - *

- * The method returns a CompletableFuture that will be completed when the user - * closes the dialog. - * - * @param repoInfo Map containing repository information with keys defined in {@code GitPropertyName} - * @return A CompletableFuture that completes with {@code true} when the user closes the dialog - */ - private CompletableFuture showRepositoryInfoDialog(Map repoInfo) { - // Create a CompletableFuture that will be completed when the user makes a choice - CompletableFuture future = new CompletableFuture<>(); - - // Show dialog on JavaFX thread - runOnFxThread(() -> { - GlassPane glassPane = new GlassPane(landingPageController.getRoot()); - - final JFXNode githubInfoNode = FXMLMvvmLoader - .make(GitHubInfoController.class.getResource("github-info.fxml")); - final Pane dialogPane = githubInfoNode.node(); - final GitHubInfoController controller = githubInfoNode.controller(); - - controller.getGitUrlTextField().setText(repoInfo.get(GIT_URL)); - controller.getGitUsernameTextField().setText(repoInfo.get(GIT_USERNAME)); - controller.getGitEmailTextField().setText(repoInfo.get(GIT_EMAIL)); - controller.getStatusTextArea().setText(repoInfo.get(GIT_STATUS)); - - controller.getCloseButton().setOnAction(_ -> { - glassPane.removeContent(dialogPane); - glassPane.hide(); - future.complete(true); // Complete with true on close - }); - - glassPane.addContent(dialogPane); - glassPane.show(); - }); - - return future; - } - - public void showAboutDialog() { - AboutDialog aboutDialog = new AboutDialog(); - aboutDialog.showAndWait(); - } - public void quit() { saveJournalWindowsToPreferences(); PrimitiveData.stop(); @@ -1110,77 +986,6 @@ public static void stopServer() { } } - private void restoreTab(KometPreferences windowPreferences, String tabPreferenceNodeName, ObservableViewNoOverride windowView, Consumer nodeConsumer) { - LOG.info("Restoring from: " + tabPreferenceNodeName); - KometPreferences itemPreferences = windowPreferences.node(KOMET_NODES + tabPreferenceNodeName); - itemPreferences.get(WindowComponent.WindowComponentKeys.FACTORY_CLASS).ifPresent(factoryClassName -> { - try { - Class objectClass = Class.forName(factoryClassName); - Class annotationClass = Reconstructor.class; - Object[] parameters = new Object[]{windowView, itemPreferences}; - WindowComponent windowComponent = (WindowComponent) Encodable.decode(objectClass, annotationClass, parameters); - nodeConsumer.accept(windowComponent.getNode()); - - } catch (Exception e) { - AlertStreams.getRoot().dispatch(AlertObject.makeError(e)); - } - }); - } - - public void createMenuOptions(BorderPane landingPageRoot) { - MenuBar menuBar = new MenuBar(); - - Menu fileMenu = new Menu("File"); - MenuItem about = new MenuItem("About"); - about.setOnAction(_ -> showAboutDialog()); - fileMenu.getItems().add(about); - - MenuItem menuItemQuit = new MenuItem("Quit"); - KeyCombination quitKeyCombo = new KeyCodeCombination(KeyCode.Q, KeyCombination.CONTROL_DOWN); - menuItemQuit.setOnAction(actionEvent -> quit()); - menuItemQuit.setAccelerator(quitKeyCombo); - fileMenu.getItems().add(menuItemQuit); - - Menu viewMenu = new Menu("View"); - MenuItem classicKometMenuItem = createClassicKometMenuItem(); - viewMenu.getItems().add(classicKometMenuItem); - - Menu windowMenu = new Menu("Window"); - MenuItem minimizeWindow = new MenuItem("Minimize"); - KeyCombination minimizeKeyCombo = new KeyCodeCombination(KeyCode.M, KeyCombination.CONTROL_DOWN); - minimizeWindow.setOnAction(event -> { - Stage obj = (Stage) landingPageRoot.getScene().getWindow(); - obj.setIconified(true); - }); - minimizeWindow.setAccelerator(minimizeKeyCombo); - minimizeWindow.setDisable(IS_BROWSER); - windowMenu.getItems().add(minimizeWindow); - - Menu exchangeMenu = createExchangeMenu(); - - menuBar.getMenus().add(fileMenu); - menuBar.getMenus().add(viewMenu); - menuBar.getMenus().add(windowMenu); - menuBar.getMenus().add(exchangeMenu); - landingPageRoot.setTop(menuBar); - } - - private MenuItem createClassicKometMenuItem() { - MenuItem classicKometMenuItem = new MenuItem("Classic Komet"); - KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN); - classicKometMenuItem.setOnAction(actionEvent -> { - try { - appClassicKomet.launchClassicKomet(); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (BackingStoreException e) { - throw new RuntimeException(e); - } - }); - classicKometMenuItem.setAccelerator(classicKometKeyCombo); - return classicKometMenuItem; - } - public enum AppKeys { APP_INITIALIZED } From 7beca33c7b4fbb7712d5926cb3ea00551cb35a8b Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Thu, 7 Aug 2025 21:08:16 +0200 Subject: [PATCH 04/10] Refactor App and WebApp to enhance modularity by removing unused methods, adding state retrieval in AppInterface, and improving menu setup in AppMenu. --- .../src/main/java/dev/ikm/komet/app/App.java | 120 +---------- .../dev/ikm/komet/app/AppClassicKomet.java | 4 +- .../java/dev/ikm/komet/app/AppInterface.java | 4 + .../main/java/dev/ikm/komet/app/AppMenu.java | 189 ++++++++++++++++++ .../main/java/dev/ikm/komet/app/WebApp.java | 183 +---------------- 5 files changed, 206 insertions(+), 294 deletions(-) diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 148b6e83e..32a35cca5 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -147,7 +147,6 @@ public class App extends Application implements AppInterface { private Stage primaryStage; private static Stage classicKometStage; - private static long windowCount = 1; // variables specific to resource overlay private Stage overlayStage; @@ -190,6 +189,11 @@ public Stage getPrimaryStage() { return primaryStage; } + @Override + public SimpleObjectProperty getState() { + return state; + } + @Override public LandingPageController getLandingPageController() { return landingPageController; @@ -223,83 +227,6 @@ public static void main(String[] args) { launch(); } - private static void createNewStage() { - Stage stage = new Stage(); - stage.setScene(new Scene(new StackPane())); - stage.setTitle("New stage" + " " + (windowCount++)); - stage.show(); - } - - private static ImmutableList makeDefaultLeftTabs(ObservableViewNoOverride windowView) { - - GraphNavigatorNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory(); - KometNode navigatorNode1 = navigatorNodeFactory.create(windowView, - ActivityStreams.NAVIGATION, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab navigatorNode1Tab = new DetachableTab(navigatorNode1); - - - PatternNavigatorFactory patternNavigatorNodeFactory = new PatternNavigatorFactory(); - - KometNode patternNavigatorNode2 = patternNavigatorNodeFactory.create(windowView, - ActivityStreams.NAVIGATION, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - - DetachableTab patternNavigatorNode1Tab = new DetachableTab(patternNavigatorNode2); - - return Lists.immutable.of(navigatorNode1Tab, patternNavigatorNode1Tab); - } - - private static ImmutableList makeDefaultCenterTabs(ObservableViewNoOverride windowView) { - - DetailsNodeFactory detailsNodeFactory = new DetailsNodeFactory(); - KometNode detailsNode1 = detailsNodeFactory.create(windowView, - ActivityStreams.NAVIGATION, ActivityStreamOption.SUBSCRIBE.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - - DetachableTab detailsNode1Tab = new DetachableTab(detailsNode1); - // TODO: setting up tab graphic, title, and tooltip needs to be standardized by the factory... - detailsNode1Tab.textProperty().bind(detailsNode1.getTitle()); - detailsNode1Tab.tooltipProperty().setValue(detailsNode1.makeToolTip()); - - KometNode detailsNode2 = detailsNodeFactory.create(windowView, - ActivityStreams.SEARCH, ActivityStreamOption.SUBSCRIBE.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab detailsNode2Tab = new DetachableTab(detailsNode2); - - KometNode detailsNode3 = detailsNodeFactory.create(windowView, - ActivityStreams.UNLINKED, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab detailsNode3Tab = new DetachableTab(detailsNode3); - - ListNodeFactory listNodeFactory = new ListNodeFactory(); - KometNode listNode = listNodeFactory.create(windowView, - ActivityStreams.LIST, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab listNodeNodeTab = new DetachableTab(listNode); - - TableNodeFactory tableNodeFactory = new TableNodeFactory(); - KometNode tableNode = tableNodeFactory.create(windowView, - ActivityStreams.UNLINKED, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab tableNodeTab = new DetachableTab(tableNode); - - return Lists.immutable.of(detailsNode1Tab, detailsNode2Tab, detailsNode3Tab, listNodeNodeTab, tableNodeTab); - } - - private static ImmutableList makeDefaultRightTabs(ObservableViewNoOverride windowView) { - - SearchNodeFactory searchNodeFactory = new SearchNodeFactory(); - KometNode searchNode = searchNodeFactory.create(windowView, - ActivityStreams.SEARCH, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab newSearchTab = new DetachableTab(searchNode); - - ProgressNodeFactory progressNodeFactory = new ProgressNodeFactory(); - KometNode kometNode = progressNodeFactory.create(windowView, - null, null, AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab progressTab = new DetachableTab(kometNode); - - CompletionNodeFactory completionNodeFactory = new CompletionNodeFactory(); - KometNode completionNode = completionNodeFactory.create(windowView, - null, null, AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab completionTab = new DetachableTab(completionNode); - - return Lists.immutable.of(newSearchTab, progressTab, completionTab); - } - public void init() { File logDirectory = new File(System.getProperty("user.home"), "Solor/komet/logs"); logDirectory.mkdirs(); @@ -651,43 +578,6 @@ public void stop() { Lists.immutable.ofAll(this.journalControllersList).forEach(JournalController::close); } - private MenuItem createMenuItem(String title) { - MenuItem menuItem = new MenuItem(title); - menuItem.setOnAction(this::handleEvent); - return menuItem; - } - - private Menu createDockMenu() { - Menu dockMenu = createSampleMenu(); - MenuItem open = new MenuItem("New Window"); - open.setGraphic(Icon.OPEN.makeIcon()); - open.setOnAction(e -> createNewStage()); - dockMenu.getItems().addAll(new SeparatorMenuItem(), open); - return dockMenu; - } - - private Menu createSampleMenu() { - Menu trayMenu = new Menu(); - trayMenu.setGraphic(Icon.TEMPORARY_FIX.makeIcon()); - MenuItem reload = new MenuItem("Reload"); - reload.setGraphic(Icon.SYNCHRONIZE_WITH_STREAM.makeIcon()); - reload.setOnAction(this::handleEvent); - MenuItem print = new MenuItem("Print"); - print.setOnAction(this::handleEvent); - - Menu share = new Menu("Share"); - MenuItem mail = new MenuItem("Mail"); - mail.setOnAction(this::handleEvent); - share.getItems().add(mail); - - trayMenu.getItems().addAll(reload, print, new SeparatorMenuItem(), share); - return trayMenu; - } - - private void handleEvent(ActionEvent actionEvent) { - LOG.debug("clicked " + actionEvent.getSource()); // NOSONAR - } - private void appStateChangeListener(ObservableValue observable, AppState oldValue, AppState newValue) { try { switch (newValue) { diff --git a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java index bfe670188..135b8e6f9 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java +++ b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java @@ -70,8 +70,6 @@ public class AppClassicKomet { private final AppInterface app; - KometPreferencesStage kometPreferencesStage; - /** * An entry point to launch the newer UI panels. */ @@ -154,7 +152,7 @@ void launchClassicKomet() throws IOException, BackingStoreException { app.getWebAPI().openStageAsTab(classicKometStage); } - kometPreferencesStage = new KometPreferencesStage(controller.windowView().makeOverridableViewProperties()); + app.getAppMenu().kometPreferencesStage = new KometPreferencesStage(controller.windowView().makeOverridableViewProperties()); windowPreferences.sync(); appPreferences.sync(); diff --git a/application/src/main/java/dev/ikm/komet/app/AppInterface.java b/application/src/main/java/dev/ikm/komet/app/AppInterface.java index a575fb768..18002e3ca 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppInterface.java +++ b/application/src/main/java/dev/ikm/komet/app/AppInterface.java @@ -3,6 +3,7 @@ import com.jpro.webapi.WebAPI; import dev.ikm.komet.kview.mvvm.model.GitHubPreferencesDao; import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.image.Image; import javafx.stage.Stage; import one.jpro.platform.auth.core.authentication.User; @@ -29,6 +30,9 @@ public interface AppInterface { WebAPI getWebAPI(); + + SimpleObjectProperty getState(); + /** * Returns the controller for the landing page. * diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java index 9d2d088f6..9a0dec5c8 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppMenu.java +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -1,26 +1,43 @@ package dev.ikm.komet.app; +import de.jangassen.MenuToolkit; +import de.jangassen.model.AppearanceMode; import dev.ikm.komet.app.aboutdialog.AboutDialog; +import dev.ikm.komet.framework.graphics.Icon; +import dev.ikm.komet.framework.preferences.KometPreferencesStage; import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.scene.Scene; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; +import javafx.scene.control.SeparatorMenuItem; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.StackPane; import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.prefs.BackingStoreException; +import static dev.ikm.komet.app.AppState.RUNNING; import static dev.ikm.komet.app.WebApp.IS_BROWSER; +import static dev.ikm.komet.app.WebApp.IS_MAC_AND_NOT_TESTFX_TEST; import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.PULL; import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.SYNC; public class AppMenu { + private static final Logger LOG = LoggerFactory.getLogger(AppMenu.class); + private final AppInterface app; + KometPreferencesStage kometPreferencesStage; + + private static long windowCount = 1; public AppMenu(AppInterface app) { this.app = app; @@ -148,4 +165,176 @@ private MenuItem createClassicKometMenuItem() { classicKometMenuItem.setAccelerator(classicKometKeyCombo); return classicKometMenuItem; } + + + void setupMenus() { + Menu kometAppMenu; + + if (IS_MAC_AND_NOT_TESTFX_TEST) { + MenuToolkit menuToolkit = MenuToolkit.toolkit(); + kometAppMenu = menuToolkit.createDefaultApplicationMenu("Komet"); + } else { + kometAppMenu = new Menu("Komet"); + } + + MenuItem prefsItem = new MenuItem("Komet preferences..."); + prefsItem.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN)); + prefsItem.setOnAction(event -> kometPreferencesStage.showPreferences()); + + if (IS_MAC_AND_NOT_TESTFX_TEST) { + kometAppMenu.getItems().add(2, prefsItem); + kometAppMenu.getItems().add(3, new SeparatorMenuItem()); + MenuItem appleQuit = kometAppMenu.getItems().getLast(); + appleQuit.setOnAction(event -> app.quit()); + } else { + kometAppMenu.getItems().addAll(prefsItem, new SeparatorMenuItem()); + } + + MenuBar menuBar = new MenuBar(kometAppMenu); + + if (app.getState().get() == RUNNING) { + Menu fileMenu = createFileMenu(); + Menu editMenu = createEditMenu(); + Menu viewMenu = createViewMenu(); + menuBar.getMenus().addAll(fileMenu, editMenu, viewMenu); + } + + if (IS_MAC_AND_NOT_TESTFX_TEST) { + MenuToolkit menuToolkit = MenuToolkit.toolkit(); + menuToolkit.setApplicationMenu(kometAppMenu); + menuToolkit.setAppearanceMode(AppearanceMode.AUTO); + menuToolkit.setDockIconMenu(createDockMenu()); + Menu windowMenu = createWindowMenuOnMacOS(); + menuToolkit.autoAddWindowMenuItems(windowMenu); + menuToolkit.setGlobalMenuBar(menuBar); + menuToolkit.setTrayMenu(createSampleMenu()); + + // Add the window menu to the menu bar + menuBar.getMenus().add(windowMenu); + } + + // Create and add the exchange menu to the menu bar + Menu exchangeMenu = createExchangeMenu(); + menuBar.getMenus().add(exchangeMenu); + + // Create and add the help menu to the menu bar + Menu helpMenu = createHelpMenu(); + menuBar.getMenus().add(helpMenu); + } + + private Menu createFileMenu() { + Menu fileMenu = new Menu("File"); + + // Import Dataset Menu Item + MenuItem importDatasetMenuItem = new MenuItem("Import Dataset..."); + importDatasetMenuItem.setOnAction(actionEvent -> app.openImport(app.getPrimaryStage())); + + // Export Dataset Menu Item + MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset..."); + exportDatasetMenuItem.setOnAction(actionEvent -> app.openExport(app.getPrimaryStage())); + + // Add menu items to the File menu + fileMenu.getItems().addAll(importDatasetMenuItem, exportDatasetMenuItem); + + if (IS_MAC_AND_NOT_TESTFX_TEST) { + // Close Window Menu Item + MenuToolkit tk = MenuToolkit.toolkit(); + MenuItem closeWindowMenuItem = tk.createCloseWindowMenuItem(); + fileMenu.getItems().addAll(new SeparatorMenuItem(), closeWindowMenuItem); + } + + return fileMenu; + } + + private Menu createEditMenu() { + Menu editMenu = new Menu("Edit"); + editMenu.getItems().addAll( + createMenuItem("Undo"), + createMenuItem("Redo"), + new SeparatorMenuItem(), + createMenuItem("Cut"), + createMenuItem("Copy"), + createMenuItem("Paste"), + createMenuItem("Select All")); + return editMenu; + } + + private Menu createViewMenu() { + Menu viewMenu = new Menu("View"); + MenuItem classicKometMenuItem = new MenuItem("Classic Komet"); + classicKometMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN)); + classicKometMenuItem.setOnAction(actionEvent -> { + try { + app.getAppClassicKomet().launchClassicKomet(); + } catch (IOException | BackingStoreException e) { + throw new RuntimeException(e); + } + }); + viewMenu.getItems().add(classicKometMenuItem); + return viewMenu; + } + + private Menu createWindowMenuOnMacOS() { + MenuToolkit menuToolkit = MenuToolkit.toolkit(); + Menu windowMenu = new Menu("Window"); + windowMenu.getItems().addAll( + menuToolkit.createMinimizeMenuItem(), + menuToolkit.createZoomMenuItem(), + menuToolkit.createCycleWindowsItem(), + new SeparatorMenuItem(), + menuToolkit.createBringAllToFrontItem()); + return windowMenu; + } + + private Menu createHelpMenu() { + Menu helpMenu = new Menu("Help"); + helpMenu.getItems().add(new MenuItem("Getting started")); + return helpMenu; + } + + + private MenuItem createMenuItem(String title) { + MenuItem menuItem = new MenuItem(title); + menuItem.setOnAction(this::handleEvent); + return menuItem; + } + + private Menu createDockMenu() { + Menu dockMenu = createSampleMenu(); + MenuItem open = new MenuItem("New Window"); + open.setGraphic(Icon.OPEN.makeIcon()); + open.setOnAction(e -> createNewStage()); + dockMenu.getItems().addAll(new SeparatorMenuItem(), open); + return dockMenu; + } + + private Menu createSampleMenu() { + Menu trayMenu = new Menu(); + trayMenu.setGraphic(Icon.TEMPORARY_FIX.makeIcon()); + MenuItem reload = new MenuItem("Reload"); + reload.setGraphic(Icon.SYNCHRONIZE_WITH_STREAM.makeIcon()); + reload.setOnAction(this::handleEvent); + MenuItem print = new MenuItem("Print"); + print.setOnAction(this::handleEvent); + + Menu share = new Menu("Share"); + MenuItem mail = new MenuItem("Mail"); + mail.setOnAction(this::handleEvent); + share.getItems().add(mail); + + trayMenu.getItems().addAll(reload, print, new SeparatorMenuItem(), share); + return trayMenu; + } + + private void handleEvent(ActionEvent actionEvent) { + LOG.debug("clicked " + actionEvent.getSource()); // NOSONAR + } + + private static void createNewStage() { + Stage stage = new Stage(); + stage.setScene(new Scene(new StackPane())); + stage.setTitle("New stage" + " " + (windowCount++)); + stage.show(); + } + } diff --git a/application/src/main/java/dev/ikm/komet/app/WebApp.java b/application/src/main/java/dev/ikm/komet/app/WebApp.java index 1533cc33c..cd9520b1f 100644 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ b/application/src/main/java/dev/ikm/komet/app/WebApp.java @@ -168,8 +168,6 @@ public class WebApp extends Application implements AppInterface { public static final SimpleObjectProperty userProperty = new SimpleObjectProperty<>(); private static Stage primaryStage; - private static long windowCount = 1; - private static KometPreferencesStage kometPreferencesStage; private static WebAPI webAPI; static final boolean IS_BROWSER = WebAPI.isBrowser(); @@ -214,6 +212,10 @@ public Stage getPrimaryStage() { return primaryStage; } @Override + public SimpleObjectProperty getState() { + return state; + } + @Override public LandingPageController getLandingPageController() { return landingPageController; } @@ -284,13 +286,6 @@ private static void addShutdownHook() { })); } - private static void createNewStage() { - Stage stage = new Stage(); - stage.setScene(new Scene(new StackPane())); - stage.setTitle("New stage" + " " + (windowCount++)); - stage.show(); - } - @Override public void init() throws Exception { LOG.info("Starting Komet"); @@ -487,133 +482,6 @@ private static boolean isTestFXTest() { return testFxTest != null && !testFxTest.isBlank(); } - private void setupMenus() { - Menu kometAppMenu; - - if (IS_MAC_AND_NOT_TESTFX_TEST) { - MenuToolkit menuToolkit = MenuToolkit.toolkit(); - kometAppMenu = menuToolkit.createDefaultApplicationMenu("Komet"); - } else { - kometAppMenu = new Menu("Komet"); - } - - MenuItem prefsItem = new MenuItem("Komet preferences..."); - prefsItem.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN)); - prefsItem.setOnAction(event -> WebApp.kometPreferencesStage.showPreferences()); - - if (IS_MAC_AND_NOT_TESTFX_TEST) { - kometAppMenu.getItems().add(2, prefsItem); - kometAppMenu.getItems().add(3, new SeparatorMenuItem()); - MenuItem appleQuit = kometAppMenu.getItems().getLast(); - appleQuit.setOnAction(event -> quit()); - } else { - kometAppMenu.getItems().addAll(prefsItem, new SeparatorMenuItem()); - } - - MenuBar menuBar = new MenuBar(kometAppMenu); - - if (state.get() == RUNNING) { - Menu fileMenu = createFileMenu(); - Menu editMenu = createEditMenu(); - Menu viewMenu = createViewMenu(); - menuBar.getMenus().addAll(fileMenu, editMenu, viewMenu); - } - - if (IS_MAC_AND_NOT_TESTFX_TEST) { - MenuToolkit menuToolkit = MenuToolkit.toolkit(); - menuToolkit.setApplicationMenu(kometAppMenu); - menuToolkit.setAppearanceMode(AppearanceMode.AUTO); - menuToolkit.setDockIconMenu(createDockMenu()); - Menu windowMenu = createWindowMenuOnMacOS(); - menuToolkit.autoAddWindowMenuItems(windowMenu); - menuToolkit.setGlobalMenuBar(menuBar); - menuToolkit.setTrayMenu(createSampleMenu()); - - // Add the window menu to the menu bar - menuBar.getMenus().add(windowMenu); - } - - // Create and add the exchange menu to the menu bar - Menu exchangeMenu = appMenu.createExchangeMenu(); - menuBar.getMenus().add(exchangeMenu); - - // Create and add the help menu to the menu bar - Menu helpMenu = createHelpMenu(); - menuBar.getMenus().add(helpMenu); - } - - private Menu createFileMenu() { - Menu fileMenu = new Menu("File"); - - // Import Dataset Menu Item - MenuItem importDatasetMenuItem = new MenuItem("Import Dataset..."); - importDatasetMenuItem.setOnAction(actionEvent -> openImport(primaryStage)); - - // Export Dataset Menu Item - MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset..."); - exportDatasetMenuItem.setOnAction(actionEvent -> openExport(primaryStage)); - - // Add menu items to the File menu - fileMenu.getItems().addAll(importDatasetMenuItem, exportDatasetMenuItem); - - if (IS_MAC_AND_NOT_TESTFX_TEST) { - // Close Window Menu Item - MenuToolkit tk = MenuToolkit.toolkit(); - MenuItem closeWindowMenuItem = tk.createCloseWindowMenuItem(); - fileMenu.getItems().addAll(new SeparatorMenuItem(), closeWindowMenuItem); - } - - return fileMenu; - } - - private Menu createEditMenu() { - Menu editMenu = new Menu("Edit"); - editMenu.getItems().addAll( - createMenuItem("Undo"), - createMenuItem("Redo"), - new SeparatorMenuItem(), - createMenuItem("Cut"), - createMenuItem("Copy"), - createMenuItem("Paste"), - createMenuItem("Select All")); - return editMenu; - } - - private Menu createViewMenu() { - Menu viewMenu = new Menu("View"); - MenuItem classicKometMenuItem = new MenuItem("Classic Komet"); - classicKometMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN)); - classicKometMenuItem.setOnAction(actionEvent -> { - try { - appClassicKomet.launchClassicKomet(); - } catch (IOException | BackingStoreException e) { - throw new RuntimeException(e); - } - }); - viewMenu.getItems().add(classicKometMenuItem); - return viewMenu; - } - - private Menu createWindowMenuOnMacOS() { - MenuToolkit menuToolkit = MenuToolkit.toolkit(); - Menu windowMenu = new Menu("Window"); - windowMenu.getItems().addAll( - menuToolkit.createMinimizeMenuItem(), - menuToolkit.createZoomMenuItem(), - menuToolkit.createCycleWindowsItem(), - new SeparatorMenuItem(), - menuToolkit.createBringAllToFrontItem()); - return windowMenu; - } - - - - private Menu createHelpMenu() { - Menu helpMenu = new Menu("Help"); - helpMenu.getItems().add(new MenuItem("Getting started")); - return helpMenu; - } - private void launchLoginPage(Stage stage) { JFXNode loginNode = FXMLMvvmLoader.make( LoginPageController.class.getResource("login-page.fxml")); @@ -621,7 +489,7 @@ private void launchLoginPage(Stage stage) { rootPane.getChildren().setAll(loginPane); stage.setTitle("KOMET Login"); - setupMenus(); + appMenu.setupMenus(); } private void launchSelectDataSourcePage(Stage stage) { @@ -637,7 +505,7 @@ private void launchSelectDataSourcePage(Stage stage) { rootPane.getChildren().setAll(sourceRoot); stage.setTitle("KOMET Startup"); - setupMenus(); + appMenu.setupMenus(); } catch (IOException ex) { LOG.error("Failed to initialize the select data source window", ex); } @@ -688,7 +556,7 @@ public void launchLandingPage(Stage stage, User user) { rootPane.getChildren().add(landingPageBorderPane); - setupMenus(); + getAppMenu().setupMenus(); } catch (IOException e) { LOG.error("Failed to initialize the landing page window", e); } @@ -842,43 +710,6 @@ private void saveJournalWindowsToPreferences() { } } - private MenuItem createMenuItem(String title) { - MenuItem menuItem = new MenuItem(title); - menuItem.setOnAction(this::handleEvent); - return menuItem; - } - - private Menu createDockMenu() { - Menu dockMenu = createSampleMenu(); - MenuItem open = new MenuItem("New Window"); - open.setGraphic(Icon.OPEN.makeIcon()); - open.setOnAction(e -> createNewStage()); - dockMenu.getItems().addAll(new SeparatorMenuItem(), open); - return dockMenu; - } - - private Menu createSampleMenu() { - Menu trayMenu = new Menu(); - trayMenu.setGraphic(Icon.TEMPORARY_FIX.makeIcon()); - MenuItem reload = new MenuItem("Reload"); - reload.setGraphic(Icon.SYNCHRONIZE_WITH_STREAM.makeIcon()); - reload.setOnAction(this::handleEvent); - MenuItem print = new MenuItem("Print"); - print.setOnAction(this::handleEvent); - - Menu share = new Menu("Share"); - MenuItem mail = new MenuItem("Mail"); - mail.setOnAction(this::handleEvent); - share.getItems().add(mail); - - trayMenu.getItems().addAll(reload, print, new SeparatorMenuItem(), share); - return trayMenu; - } - - private void handleEvent(ActionEvent actionEvent) { - LOG.debug("clicked " + actionEvent.getSource()); // NOSONAR - } - private void appStateChangeListener(ObservableValue observable, AppState oldValue, AppState newValue) { try { switch (newValue) { From ab394f79104d65a760cb7ec603788657102408e0 Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Thu, 7 Aug 2025 21:20:32 +0200 Subject: [PATCH 05/10] Refactor App and WebApp to centralize import and export functionality in AppMenu, removing redundant methods and enhancing code maintainability. --- .../src/main/java/dev/ikm/komet/app/App.java | 43 ------------- .../java/dev/ikm/komet/app/AppInterface.java | 6 -- .../main/java/dev/ikm/komet/app/AppMenu.java | 60 +++++++++++++++++-- .../main/java/dev/ikm/komet/app/WebApp.java | 40 +------------ 4 files changed, 57 insertions(+), 92 deletions(-) diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 32a35cca5..d01420724 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -600,49 +600,6 @@ private void appStateChangeListener(ObservableValue observab } } - private void openImport(FrameworkTopics destinationTopic) { - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - WindowSettings windowSettings = new WindowSettings(windowPreferences); - Stage importStage = new Stage(StageStyle.TRANSPARENT); - importStage.initOwner(getFocusedWindow()); - //set up ImportViewModel - Config importConfig = new Config(ImportController.class.getResource("import.fxml")) - .updateViewModel("importViewModel", importViewModel -> - importViewModel - .setPropertyValue(VIEW_PROPERTIES, windowSettings.getView().makeOverridableViewProperties()) - .setPropertyValue(DESTINATION_TOPIC, destinationTopic)); - JFXNode importJFXNode = FXMLMvvmLoader.make(importConfig); - - Pane importPane = importJFXNode.node(); - Scene importScene = new Scene(importPane, Color.TRANSPARENT); - importStage.setScene(importScene); - importStage.show(); - } - - public void openImport(Stage stage) { - openImport(PROGRESS_TOPIC); - } - - public void openExport(Stage stage) { - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - WindowSettings windowSettings = new WindowSettings(windowPreferences); - Stage exportStage = new Stage(StageStyle.TRANSPARENT); - exportStage.initOwner(getFocusedWindow()); - //set up ExportViewModel - Config exportConfig = new Config(ExportController.class.getResource("export.fxml")) - .updateViewModel("exportViewModel", (exportViewModel) -> - exportViewModel.setPropertyValue(VIEW_PROPERTIES, - windowSettings.getView().makeOverridableViewProperties())); - JFXNode exportJFXNode = FXMLMvvmLoader.make(exportConfig); - - Pane exportPane = exportJFXNode.node(); - Scene exportScene = new Scene(exportPane, Color.TRANSPARENT); - exportStage.setScene(exportScene); - exportStage.show(); - } - public void quit() { saveJournalWindowsToPreferences(); PrimitiveData.stop(); diff --git a/application/src/main/java/dev/ikm/komet/app/AppInterface.java b/application/src/main/java/dev/ikm/komet/app/AppInterface.java index 18002e3ca..108dfa96e 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppInterface.java +++ b/application/src/main/java/dev/ikm/komet/app/AppInterface.java @@ -54,11 +54,5 @@ public interface AppInterface { void launchLandingPage(Stage stage, User user); - //void showAboutDialog(); - - void openImport(Stage stage); - - void openExport(Stage stage); - Stage getPrimaryStage(); } diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java index 9a0dec5c8..0bd96bc8b 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppMenu.java +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -5,6 +5,11 @@ import dev.ikm.komet.app.aboutdialog.AboutDialog; import dev.ikm.komet.framework.graphics.Icon; import dev.ikm.komet.framework.preferences.KometPreferencesStage; +import dev.ikm.komet.framework.window.WindowSettings; +import dev.ikm.komet.kview.mvvm.view.changeset.ExportController; +import dev.ikm.komet.kview.mvvm.view.changeset.ImportController; +import dev.ikm.komet.preferences.KometPreferences; +import dev.ikm.komet.preferences.KometPreferencesImpl; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.scene.Scene; @@ -16,8 +21,14 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; import javafx.stage.Stage; +import javafx.stage.StageStyle; +import org.carlfx.cognitive.loader.Config; +import org.carlfx.cognitive.loader.FXMLMvvmLoader; +import org.carlfx.cognitive.loader.JFXNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,6 +40,8 @@ import static dev.ikm.komet.app.WebApp.IS_MAC_AND_NOT_TESTFX_TEST; import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.PULL; import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.SYNC; +import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.VIEW_PROPERTIES; +import static dev.ikm.komet.preferences.JournalWindowPreferences.MAIN_KOMET_WINDOW; public class AppMenu { @@ -53,12 +66,12 @@ void generateMsWindowsMenu(BorderPane kometRoot, Stage stage) { // Importing data MenuItem importMenuItem = new MenuItem("Import Dataset"); - importMenuItem.setOnAction(actionEvent -> app.openImport(stage)); + importMenuItem.setOnAction(actionEvent -> openImport(stage)); fileMenu.getItems().add(importMenuItem); // Exporting data MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset"); - exportDatasetMenuItem.setOnAction(actionEvent -> app.openExport(stage)); + exportDatasetMenuItem.setOnAction(actionEvent -> openExport(stage)); fileMenu.getItems().add(exportDatasetMenuItem); MenuItem menuItemQuit = new MenuItem("Quit"); @@ -227,11 +240,11 @@ private Menu createFileMenu() { // Import Dataset Menu Item MenuItem importDatasetMenuItem = new MenuItem("Import Dataset..."); - importDatasetMenuItem.setOnAction(actionEvent -> app.openImport(app.getPrimaryStage())); + importDatasetMenuItem.setOnAction(actionEvent -> openImport(app.getPrimaryStage())); // Export Dataset Menu Item MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset..."); - exportDatasetMenuItem.setOnAction(actionEvent -> app.openExport(app.getPrimaryStage())); + exportDatasetMenuItem.setOnAction(actionEvent -> openExport(app.getPrimaryStage())); // Add menu items to the File menu fileMenu.getItems().addAll(importDatasetMenuItem, exportDatasetMenuItem); @@ -337,4 +350,43 @@ private static void createNewStage() { stage.show(); } + + public void openImport(Stage owner) { + KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); + KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); + WindowSettings windowSettings = new WindowSettings(windowPreferences); + Stage importStage = new Stage(StageStyle.TRANSPARENT); + importStage.initOwner(owner); + //set up ImportViewModel + Config importConfig = new Config(ImportController.class.getResource("import.fxml")) + .updateViewModel("importViewModel", (importViewModel) -> + importViewModel.setPropertyValue(VIEW_PROPERTIES, + windowSettings.getView().makeOverridableViewProperties())); + JFXNode importJFXNode = FXMLMvvmLoader.make(importConfig); + + Pane importPane = importJFXNode.node(); + Scene importScene = new Scene(importPane, Color.TRANSPARENT); + importStage.setScene(importScene); + importStage.show(); + } + + public void openExport(Stage owner) { + KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); + KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); + WindowSettings windowSettings = new WindowSettings(windowPreferences); + Stage exportStage = new Stage(StageStyle.TRANSPARENT); + exportStage.initOwner(owner); + //set up ExportViewModel + Config exportConfig = new Config(ExportController.class.getResource("export.fxml")) + .updateViewModel("exportViewModel", (exportViewModel) -> + exportViewModel.setPropertyValue(VIEW_PROPERTIES, + windowSettings.getView().makeOverridableViewProperties())); + JFXNode exportJFXNode = FXMLMvvmLoader.make(exportConfig); + + Pane exportPane = exportJFXNode.node(); + Scene exportScene = new Scene(exportPane, Color.TRANSPARENT); + exportStage.setScene(exportScene); + exportStage.show(); + } + } diff --git a/application/src/main/java/dev/ikm/komet/app/WebApp.java b/application/src/main/java/dev/ikm/komet/app/WebApp.java index cd9520b1f..5ef3e700f 100644 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ b/application/src/main/java/dev/ikm/komet/app/WebApp.java @@ -349,7 +349,7 @@ public void init() throws Exception { //Pops up the import dialog window on any events received on the IMPORT_TOPIC Subscriber importSubscriber = _ -> { - openImport(primaryStage); + appMenu.openImport(primaryStage); }; kViewEventBus.subscribe(IMPORT_TOPIC, Evt.class, importSubscriber); } @@ -733,44 +733,6 @@ private void appStateChangeListener(ObservableValue observab } } - public void openImport(Stage owner) { - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - WindowSettings windowSettings = new WindowSettings(windowPreferences); - Stage importStage = new Stage(StageStyle.TRANSPARENT); - importStage.initOwner(owner); - //set up ImportViewModel - Config importConfig = new Config(ImportController.class.getResource("import.fxml")) - .updateViewModel("importViewModel", (importViewModel) -> - importViewModel.setPropertyValue(VIEW_PROPERTIES, - windowSettings.getView().makeOverridableViewProperties())); - JFXNode importJFXNode = FXMLMvvmLoader.make(importConfig); - - Pane importPane = importJFXNode.node(); - Scene importScene = new Scene(importPane, Color.TRANSPARENT); - importStage.setScene(importScene); - importStage.show(); - } - - public void openExport(Stage owner) { - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - WindowSettings windowSettings = new WindowSettings(windowPreferences); - Stage exportStage = new Stage(StageStyle.TRANSPARENT); - exportStage.initOwner(owner); - //set up ExportViewModel - Config exportConfig = new Config(ExportController.class.getResource("export.fxml")) - .updateViewModel("exportViewModel", (exportViewModel) -> - exportViewModel.setPropertyValue(VIEW_PROPERTIES, - windowSettings.getView().makeOverridableViewProperties())); - JFXNode exportJFXNode = FXMLMvvmLoader.make(exportConfig); - - Pane exportPane = exportJFXNode.node(); - Scene exportScene = new Scene(exportPane, Color.TRANSPARENT); - exportStage.setScene(exportScene); - exportStage.show(); - } - public void quit() { saveJournalWindowsToPreferences(); PrimitiveData.stop(); From a96f1a849c4b1695d688fc2e9e09f718e2256519 Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Thu, 7 Aug 2025 21:40:23 +0200 Subject: [PATCH 06/10] Moved code for the creation of the pages of the app in a separate class. --- .../src/main/java/dev/ikm/komet/app/App.java | 115 +++--------- .../java/dev/ikm/komet/app/AppInterface.java | 15 ++ .../main/java/dev/ikm/komet/app/AppPages.java | 172 ++++++++++++++++++ .../main/java/dev/ikm/komet/app/WebApp.java | 150 +++------------ 4 files changed, 236 insertions(+), 216 deletions(-) create mode 100644 application/src/main/java/dev/ikm/komet/app/AppPages.java diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index d01420724..09ac842ca 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -158,6 +158,7 @@ public class App extends Application implements AppInterface { AppGithub appGithub; AppClassicKomet appClassicKomet; AppMenu appMenu; + AppPages appPages; @Override public AppGithub getAppGithub() { @@ -194,6 +195,11 @@ public SimpleObjectProperty getState() { return state; } + @Override + public StackPane getRootPane() { + return null; + } + @Override public LandingPageController getLandingPageController() { return landingPageController; @@ -204,6 +210,16 @@ public GitHubPreferencesDao getGitHubPreferencesDao() { return gitHubPreferencesDao; } + @Override + public List getJournalControllersList() { + return journalControllersList; + } + + @Override + public EvtBus getKViewEventBus() { + return kViewEventBus; + } + /** * This is a list of new windows that have been launched. During shutdown, the application close each stage gracefully. */ @@ -246,7 +262,7 @@ public void init() { .findFirst() .ifPresentOrElse( JournalController::windowToFront, /* Window already launched now make window to the front (so user sees window) */ - () -> launchJournalViewPage(journalWindowSettingsObjectMap) /* launch new Journal view window */ + () -> appPages.launchJournalViewPage(journalWindowSettingsObjectMap) /* launch new Journal view window */ ); }; // subscribe to the topic @@ -260,6 +276,7 @@ public void start(Stage stage) { appGithub = new AppGithub(this); appClassicKomet = new AppClassicKomet(this); appMenu = new AppMenu(this); + appPages = new AppPages(this); try { primaryStage = stage; @@ -424,94 +441,6 @@ public void launchLandingPage(Stage stage, User user) { kViewStage.setMaximized(true); } - /** - * When a user selects the menu option View/New Journal a new Stage Window is launched. - * This method will load a navigation panel to be a publisher and windows will be connected (subscribed) to the activity stream. - * - * @param journalWindowSettings if present will give the size and positioning of the journal window - */ - private void launchJournalViewPage(PrefX journalWindowSettings) { - Objects.requireNonNull(journalWindowSettings, "journalWindowSettings cannot be null"); - final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - final KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - final WindowSettings windowSettings = new WindowSettings(windowPreferences); - final UUID journalTopic = journalWindowSettings.getValue(JOURNAL_TOPIC); - Objects.requireNonNull(journalTopic, "journalTopic cannot be null"); - - // Ask service loader for a journal window factory. - Stage journalStageWindow = new Stage(); - Config journalConfig = new Config(JournalController.class.getResource("journal.fxml")) - .updateViewModel("journalViewModel", journalViewModel -> { - journalViewModel.setPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC, journalTopic); - journalViewModel.setPropertyValue(WINDOW_VIEW, windowSettings.getView()); - }); - JFXNode journalJFXNode = FXMLMvvmLoader.make(journalConfig); - BorderPane journalBorderPane = journalJFXNode.node(); - JournalController journalController = journalJFXNode.controller(); - journalController.setup(windowPreferences); - Scene sourceScene = new Scene(journalBorderPane, DEFAULT_JOURNAL_WIDTH, DEFAULT_JOURNAL_HEIGHT); - addStylesheets(sourceScene, KOMET_CSS, KVIEW_CSS); - - journalStageWindow.setScene(sourceScene); - // if NOT on Mac OS - if (System.getProperty("os.name") != null && !System.getProperty("os.name").toLowerCase().startsWith(OS_NAME_MAC)) { - appMenu.generateMsWindowsMenu(journalBorderPane, journalStageWindow); - } - - // load journal specific window settings - final String journalName = journalWindowSettings.getValue(JOURNAL_TITLE); - journalStageWindow.setTitle(journalName); - - // Get the UUID-based directory name from preferences - String journalDirName = journalWindowSettings.getValue(JOURNAL_DIR_NAME); - - // For new journals (no UUID yet), generate one using the controller's UUID - if (journalDirName == null) { - journalDirName = journalController.getJournalDirName(); - journalWindowSettings.setValue(JOURNAL_DIR_NAME, journalDirName); - } - - if (journalWindowSettings.getValue(JOURNAL_HEIGHT) != null) { - journalStageWindow.setHeight(journalWindowSettings.getValue(JOURNAL_HEIGHT)); - journalStageWindow.setWidth(journalWindowSettings.getValue(JOURNAL_WIDTH)); - journalStageWindow.setX(journalWindowSettings.getValue(JOURNAL_XPOS)); - journalStageWindow.setY(journalWindowSettings.getValue(JOURNAL_YPOS)); - journalController.restoreWindowsAsync(journalWindowSettings); - } else { - journalStageWindow.setMaximized(true); - } - - journalStageWindow.setOnHidden(windowEvent -> { - saveJournalWindowsToPreferences(); - // call shutdown method on the view - journalController.shutdown(); - journalControllersList.remove(journalController); - // enable Delete menu option - journalWindowSettings.setValue(CAN_DELETE, true); - kViewEventBus.publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); - }); - - // Launch windows window pane inside journal view - journalStageWindow.setOnShown(windowEvent -> { - //TODO: Refactor factory constructor calls below to use PluggableService (make constructors private) - KometNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory(); - KometNodeFactory searchNodeFactory = new SearchNodeFactory(); - - journalController.launchKometFactoryNodes( - journalWindowSettings.getValue(JOURNAL_TITLE), - navigatorNodeFactory, - searchNodeFactory); - // load additional panels - journalController.loadNextGenReasonerPanel(); - journalController.loadNextGenSearchPanel(); - }); - // disable the delete menu option for a Journal Card. - journalWindowSettings.setValue(CAN_DELETE, false); - kViewEventBus.publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); - journalControllersList.add(journalController); - journalStageWindow.show(); - } - /** * Saves the current state of the journals and its windows to the application's preferences system. *

@@ -533,7 +462,7 @@ private void launchJournalViewPage(PrefX journalWindowSettings) { * └── Window states * */ - private void saveJournalWindowsToPreferences() { + public void saveJournalWindowsToPreferences() { final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); final KometPreferences journalsPreferences = appPreferences.node(JOURNALS); @@ -690,4 +619,10 @@ private void updateResourceUsage(final Label cpuUsageLabel, final Label memoryUs public enum AppKeys { APP_INITIALIZED } + + @Override + public void stopServer() { + // No server to stop in this application + LOG.info("No server to stop in this application."); + } } \ No newline at end of file diff --git a/application/src/main/java/dev/ikm/komet/app/AppInterface.java b/application/src/main/java/dev/ikm/komet/app/AppInterface.java index 108dfa96e..6f09e3870 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppInterface.java +++ b/application/src/main/java/dev/ikm/komet/app/AppInterface.java @@ -1,13 +1,18 @@ package dev.ikm.komet.app; import com.jpro.webapi.WebAPI; +import dev.ikm.komet.framework.events.EvtBus; import dev.ikm.komet.kview.mvvm.model.GitHubPreferencesDao; +import dev.ikm.komet.kview.mvvm.view.journal.JournalController; import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.image.Image; +import javafx.scene.layout.StackPane; import javafx.stage.Stage; import one.jpro.platform.auth.core.authentication.User; +import java.util.List; + /** * Contains the shared elements between App and WebApp. * Ideally, this will be removed and only one App is left - because they become the same. @@ -47,12 +52,22 @@ public interface AppInterface { */ GitHubPreferencesDao getGitHubPreferencesDao(); + void saveJournalWindowsToPreferences(); + + List getJournalControllersList(); + /** * Quits the application. */ void quit(); + void stopServer(); + + StackPane getRootPane(); + void launchLandingPage(Stage stage, User user); Stage getPrimaryStage(); + + EvtBus getKViewEventBus(); } diff --git a/application/src/main/java/dev/ikm/komet/app/AppPages.java b/application/src/main/java/dev/ikm/komet/app/AppPages.java new file mode 100644 index 000000000..51077f043 --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java @@ -0,0 +1,172 @@ +package dev.ikm.komet.app; + +import dev.ikm.komet.framework.KometNodeFactory; +import dev.ikm.komet.framework.preferences.PrefX; +import dev.ikm.komet.framework.window.WindowSettings; +import dev.ikm.komet.kview.events.JournalTileEvent; +import dev.ikm.komet.kview.mvvm.view.journal.JournalController; +import dev.ikm.komet.kview.mvvm.view.login.LoginPageController; +import dev.ikm.komet.navigator.graph.GraphNavigatorNodeFactory; +import dev.ikm.komet.preferences.KometPreferences; +import dev.ikm.komet.preferences.KometPreferencesImpl; +import dev.ikm.komet.search.SearchNodeFactory; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; +import org.carlfx.cognitive.loader.Config; +import org.carlfx.cognitive.loader.FXMLMvvmLoader; +import org.carlfx.cognitive.loader.JFXNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; + +import static dev.ikm.komet.app.WebApp.IS_BROWSER; +import static dev.ikm.komet.app.WebApp.IS_MAC; +import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; +import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; +import static dev.ikm.komet.app.util.CssUtils.addStylesheets; +import static dev.ikm.komet.kview.events.EventTopics.JOURNAL_TOPIC; +import static dev.ikm.komet.kview.events.JournalTileEvent.UPDATE_JOURNAL_TILE; +import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.CURRENT_JOURNAL_WINDOW_TOPIC; +import static dev.ikm.komet.kview.mvvm.viewmodel.JournalViewModel.WINDOW_VIEW; +import static dev.ikm.komet.preferences.JournalWindowPreferences.*; +import static dev.ikm.komet.preferences.JournalWindowSettings.*; +import static dev.ikm.komet.preferences.JournalWindowSettings.CAN_DELETE; + +public class AppPages { + private static final Logger LOG = LoggerFactory.getLogger(AppGithub.class); + + private final AppInterface app; + + public AppPages(AppInterface app) { + this.app = app; + } + + + void launchLoginPage(Stage stage) { + JFXNode loginNode = FXMLMvvmLoader.make( + LoginPageController.class.getResource("login-page.fxml")); + BorderPane loginPane = loginNode.node(); + app.getRootPane().getChildren().setAll(loginPane); + stage.setTitle("KOMET Login"); + + app.getAppMenu().setupMenus(); + } + + void launchSelectDataSourcePage(Stage stage) { + try { + FXMLLoader sourceLoader = new FXMLLoader(getClass().getResource("SelectDataSource.fxml")); + BorderPane sourceRoot = sourceLoader.load(); + SelectDataSourceController sourceController = sourceLoader.getController(); + sourceController.getCancelButton().setOnAction(actionEvent -> { + // Exit the application if the user cancels the data source selection + Platform.exit(); + app.stopServer(); + }); + app.getRootPane().getChildren().setAll(sourceRoot); + stage.setTitle("KOMET Startup"); + + app.getAppMenu().setupMenus(); + } catch (IOException ex) { + LOG.error("Failed to initialize the select data source window", ex); + } + } + + + /** + * When a user selects the menu option View/New Journal a new Stage Window is launched. + * This method will load a navigation panel to be a publisher and windows will be connected + * (subscribed) to the activity stream. + * + * @param journalWindowSettings if present will give the size and positioning of the journal window + */ + void launchJournalViewPage(PrefX journalWindowSettings) { + Objects.requireNonNull(journalWindowSettings, "journalWindowSettings cannot be null"); + final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); + final KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); + final WindowSettings windowSettings = new WindowSettings(windowPreferences); + final UUID journalTopic = journalWindowSettings.getValue(JOURNAL_TOPIC); + Objects.requireNonNull(journalTopic, "journalTopic cannot be null"); + + Config journalConfig = new Config(JournalController.class.getResource("journal.fxml")) + .updateViewModel("journalViewModel", journalViewModel -> { + journalViewModel.setPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC, journalTopic); + journalViewModel.setPropertyValue(WINDOW_VIEW, windowSettings.getView()); + }); + JFXNode journalJFXNode = FXMLMvvmLoader.make(journalConfig); + BorderPane journalBorderPane = journalJFXNode.node(); + JournalController journalController = journalJFXNode.controller(); + + Scene sourceScene = new Scene(journalBorderPane, DEFAULT_JOURNAL_WIDTH, DEFAULT_JOURNAL_HEIGHT); + addStylesheets(sourceScene, KOMET_CSS, KVIEW_CSS); + + Stage journalStage = new Stage(); + journalStage.getIcons().setAll(app.getAppIcon()); + journalStage.setScene(sourceScene); + + if (!IS_MAC) { + app.getAppMenu().generateMsWindowsMenu(journalBorderPane, journalStage); + } + + // load journal specific window settings + final String journalName = journalWindowSettings.getValue(JOURNAL_TITLE); + journalStage.setTitle(journalName); + + // Get the UUID-based directory name from preferences + String journalDirName = journalWindowSettings.getValue(JOURNAL_DIR_NAME); + + // For new journals (no UUID yet), generate one using the controller's UUID + if (journalDirName == null) { + journalDirName = journalController.getJournalDirName(); + journalWindowSettings.setValue(JOURNAL_DIR_NAME, journalDirName); + } + + if (journalWindowSettings.getValue(JOURNAL_HEIGHT) != null) { + journalStage.setHeight(journalWindowSettings.getValue(JOURNAL_HEIGHT)); + journalStage.setWidth(journalWindowSettings.getValue(JOURNAL_WIDTH)); + journalStage.setX(journalWindowSettings.getValue(JOURNAL_XPOS)); + journalStage.setY(journalWindowSettings.getValue(JOURNAL_YPOS)); + journalController.restoreWindows(journalWindowSettings); + } else { + journalStage.setMaximized(true); + } + + journalStage.setOnHidden(windowEvent -> { + app.saveJournalWindowsToPreferences(); + journalController.shutdown(); + app.getJournalControllersList().remove(journalController); + + journalWindowSettings.setValue(CAN_DELETE, true); + app.getKViewEventBus().publish(JOURNAL_TOPIC, + new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); + }); + + journalStage.setOnShown(windowEvent -> { + KometNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory(); + KometNodeFactory searchNodeFactory = new SearchNodeFactory(); + + journalController.launchKometFactoryNodes( + journalWindowSettings.getValue(JOURNAL_TITLE), + navigatorNodeFactory, + searchNodeFactory); + // load additional panels + journalController.loadNextGenReasonerPanel(); + journalController.loadNextGenSearchPanel(); + }); + // disable the delete menu option for a Journal Card. + journalWindowSettings.setValue(CAN_DELETE, false); + app.getKViewEventBus().publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); + app.getJournalControllersList().add(journalController); + + if (IS_BROWSER) { + app.getWebAPI().openStageAsTab(journalStage, journalName.replace(" ", "_")); + } else { + journalStage.show(); + } + } +} diff --git a/application/src/main/java/dev/ikm/komet/app/WebApp.java b/application/src/main/java/dev/ikm/komet/app/WebApp.java index 5ef3e700f..d21ca0d24 100644 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ b/application/src/main/java/dev/ikm/komet/app/WebApp.java @@ -182,6 +182,7 @@ public class WebApp extends Application implements AppInterface { AppGithub appGithub; AppClassicKomet appClassicKomet; AppMenu appMenu; + AppPages appPages; @Override public AppGithub getAppGithub() { @@ -215,6 +216,12 @@ public Stage getPrimaryStage() { public SimpleObjectProperty getState() { return state; } + + @Override + public StackPane getRootPane() { + return rootPane; + } + @Override public LandingPageController getLandingPageController() { return landingPageController; @@ -225,6 +232,16 @@ public GitHubPreferencesDao getGitHubPreferencesDao() { return gitHubPreferencesDao; } + @Override + public List getJournalControllersList() { + return journalControllersList; + } + + @Override + public EvtBus getKViewEventBus() { + return kViewEventBus; + } + /** * An entry point to launch the newer UI panels. */ @@ -325,7 +342,7 @@ public void init() throws Exception { journalController.windowToFront(); } }, - () -> launchJournalViewPage(journalWindowSettingsObjectMap)); + () -> appPages.launchJournalViewPage(journalWindowSettingsObjectMap)); }; // Subscribe the subscriber to the JOURNAL_TOPIC @@ -340,7 +357,7 @@ public void init() throws Exception { } else { state.set(AppState.SELECT_DATA_SOURCE); state.addListener(this::appStateChangeListener); - launchSelectDataSourcePage(primaryStage); + appPages.launchSelectDataSourcePage(primaryStage); } }; @@ -359,6 +376,7 @@ public void start(Stage stage) { appGithub = new AppGithub(this); appClassicKomet = new AppClassicKomet(this); appMenu = new AppMenu(this); + appPages = new AppPages(this); try { WebApp.primaryStage = stage; @@ -428,7 +446,7 @@ public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) { */ private void startLogin(Stage stage) { state.set(LOGIN); - launchLoginPage(stage); + appPages.launchLoginPage(stage); } /** @@ -441,7 +459,7 @@ private void startLogin(Stage stage) { private void startSelectDataSource(Stage stage) { state.set(SELECT_DATA_SOURCE); state.addListener(this::appStateChangeListener); - launchSelectDataSourcePage(stage); + appPages.launchSelectDataSourcePage(stage); } @Override @@ -482,34 +500,6 @@ private static boolean isTestFXTest() { return testFxTest != null && !testFxTest.isBlank(); } - private void launchLoginPage(Stage stage) { - JFXNode loginNode = FXMLMvvmLoader.make( - LoginPageController.class.getResource("login-page.fxml")); - BorderPane loginPane = loginNode.node(); - rootPane.getChildren().setAll(loginPane); - stage.setTitle("KOMET Login"); - - appMenu.setupMenus(); - } - - private void launchSelectDataSourcePage(Stage stage) { - try { - FXMLLoader sourceLoader = new FXMLLoader(getClass().getResource("SelectDataSource.fxml")); - BorderPane sourceRoot = sourceLoader.load(); - SelectDataSourceController sourceController = sourceLoader.getController(); - sourceController.getCancelButton().setOnAction(actionEvent -> { - // Exit the application if the user cancels the data source selection - Platform.exit(); - stopServer(); - }); - rootPane.getChildren().setAll(sourceRoot); - stage.setTitle("KOMET Startup"); - - appMenu.setupMenus(); - } catch (IOException ex) { - LOG.error("Failed to initialize the select data source window", ex); - } - } private void addEventFilters(Stage stage) { stage.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { @@ -562,98 +552,6 @@ public void launchLandingPage(Stage stage, User user) { } } - /** - * When a user selects the menu option View/New Journal a new Stage Window is launched. - * This method will load a navigation panel to be a publisher and windows will be connected - * (subscribed) to the activity stream. - * - * @param journalWindowSettings if present will give the size and positioning of the journal window - */ - private void launchJournalViewPage(PrefX journalWindowSettings) { - Objects.requireNonNull(journalWindowSettings, "journalWindowSettings cannot be null"); - final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - final KometPreferences windowPreferences = appPreferences.node(MAIN_KOMET_WINDOW); - final WindowSettings windowSettings = new WindowSettings(windowPreferences); - final UUID journalTopic = journalWindowSettings.getValue(JOURNAL_TOPIC); - Objects.requireNonNull(journalTopic, "journalTopic cannot be null"); - - Config journalConfig = new Config(JournalController.class.getResource("journal.fxml")) - .updateViewModel("journalViewModel", journalViewModel -> { - journalViewModel.setPropertyValue(CURRENT_JOURNAL_WINDOW_TOPIC, journalTopic); - journalViewModel.setPropertyValue(WINDOW_VIEW, windowSettings.getView()); - }); - JFXNode journalJFXNode = FXMLMvvmLoader.make(journalConfig); - BorderPane journalBorderPane = journalJFXNode.node(); - JournalController journalController = journalJFXNode.controller(); - - Scene sourceScene = new Scene(journalBorderPane, DEFAULT_JOURNAL_WIDTH, DEFAULT_JOURNAL_HEIGHT); - addStylesheets(sourceScene, KOMET_CSS, KVIEW_CSS); - - Stage journalStage = new Stage(); - journalStage.getIcons().setAll(appIcon); - journalStage.setScene(sourceScene); - - if (!IS_MAC) { - appMenu.generateMsWindowsMenu(journalBorderPane, journalStage); - } - - // load journal specific window settings - final String journalName = journalWindowSettings.getValue(JOURNAL_TITLE); - journalStage.setTitle(journalName); - - // Get the UUID-based directory name from preferences - String journalDirName = journalWindowSettings.getValue(JOURNAL_DIR_NAME); - - // For new journals (no UUID yet), generate one using the controller's UUID - if (journalDirName == null) { - journalDirName = journalController.getJournalDirName(); - journalWindowSettings.setValue(JOURNAL_DIR_NAME, journalDirName); - } - - if (journalWindowSettings.getValue(JOURNAL_HEIGHT) != null) { - journalStage.setHeight(journalWindowSettings.getValue(JOURNAL_HEIGHT)); - journalStage.setWidth(journalWindowSettings.getValue(JOURNAL_WIDTH)); - journalStage.setX(journalWindowSettings.getValue(JOURNAL_XPOS)); - journalStage.setY(journalWindowSettings.getValue(JOURNAL_YPOS)); - journalController.restoreWindows(journalWindowSettings); - } else { - journalStage.setMaximized(true); - } - - journalStage.setOnHidden(windowEvent -> { - saveJournalWindowsToPreferences(); - journalController.shutdown(); - journalControllersList.remove(journalController); - - journalWindowSettings.setValue(CAN_DELETE, true); - kViewEventBus.publish(JOURNAL_TOPIC, - new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); - }); - - journalStage.setOnShown(windowEvent -> { - KometNodeFactory navigatorNodeFactory = new GraphNavigatorNodeFactory(); - KometNodeFactory searchNodeFactory = new SearchNodeFactory(); - - journalController.launchKometFactoryNodes( - journalWindowSettings.getValue(JOURNAL_TITLE), - navigatorNodeFactory, - searchNodeFactory); - // load additional panels - journalController.loadNextGenReasonerPanel(); - journalController.loadNextGenSearchPanel(); - }); - // disable the delete menu option for a Journal Card. - journalWindowSettings.setValue(CAN_DELETE, false); - kViewEventBus.publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); - journalControllersList.add(journalController); - - if (IS_BROWSER) { - webAPI.openStageAsTab(journalStage, journalName.replace(" ", "_")); - } else { - journalStage.show(); - } - } - /** * Saves the current state of the journals and its windows to the application's preferences system. *

@@ -675,7 +573,7 @@ private void launchJournalViewPage(PrefX journalWindowSettings) { * └── Window states * */ - private void saveJournalWindowsToPreferences() { + public void saveJournalWindowsToPreferences() { final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); final KometPreferences journalsPreferences = appPreferences.node(JOURNALS); @@ -744,7 +642,7 @@ public void quit() { /** * Stops the JPro server by running the stop script. */ - public static void stopServer() { + public void stopServer() { if (IS_BROWSER) { LOG.info("Stopping JPro server..."); final String jproMode = WebAPI.getServerInfo().getJProMode(); From 82786960d180f8c79f097f5cd2d18de77c8cbcea Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Thu, 7 Aug 2025 21:46:33 +0200 Subject: [PATCH 07/10] Merged App and WebApp, now only App exists --- application/pom.xml | 2 +- .../src/main/java/dev/ikm/komet/app/App.java | 641 +++++++++--------- .../dev/ikm/komet/app/AppClassicKomet.java | 18 +- .../main/java/dev/ikm/komet/app/AppMenu.java | 4 +- .../main/java/dev/ikm/komet/app/AppPages.java | 4 +- .../komet/app/SelectDataSourceController.java | 2 - 6 files changed, 325 insertions(+), 346 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 7d336e1a0..aced97195 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -549,7 +549,7 @@ one.jpro jpro-maven-plugin - dev.ikm.komet.app.WebApp + dev.ikm.komet.app.App true komet diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index 09ac842ca..d67f4b22c 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -16,144 +16,106 @@ package dev.ikm.komet.app; import com.jpro.webapi.WebAPI; -import com.sun.management.OperatingSystemMXBean; -import de.jangassen.MenuToolkit; -import de.jangassen.model.AppearanceMode; -import dev.ikm.komet.app.aboutdialog.AboutDialog; -import dev.ikm.komet.details.DetailsNodeFactory; -import dev.ikm.komet.framework.KometNode; -import dev.ikm.komet.framework.KometNodeFactory; import dev.ikm.komet.framework.ScreenInfo; -import dev.ikm.komet.framework.activity.ActivityStreamOption; -import dev.ikm.komet.framework.activity.ActivityStreams; -import dev.ikm.komet.framework.events.*; -import dev.ikm.komet.framework.graphics.Icon; +import dev.ikm.komet.framework.events.Evt; +import dev.ikm.komet.framework.events.EvtBus; +import dev.ikm.komet.framework.events.EvtBusFactory; +import dev.ikm.komet.framework.events.Subscriber; import dev.ikm.komet.framework.graphics.LoadFonts; -import dev.ikm.komet.framework.preferences.KometPreferencesStage; import dev.ikm.komet.framework.preferences.PrefX; -import dev.ikm.komet.framework.preferences.Reconstructor; -import dev.ikm.komet.framework.progress.ProgressHelper; -import dev.ikm.komet.framework.tabs.DetachableTab; -import dev.ikm.komet.framework.view.ObservableViewNoOverride; -import dev.ikm.komet.framework.window.KometStageController; -import dev.ikm.komet.framework.window.MainWindowRecord; -import dev.ikm.komet.framework.window.WindowComponent; import dev.ikm.komet.framework.window.WindowSettings; -import dev.ikm.komet.kview.controls.GlassPane; import dev.ikm.komet.kview.events.CreateJournalEvent; -import dev.ikm.komet.kview.events.JournalTileEvent; +import dev.ikm.komet.kview.events.SignInUserEvent; import dev.ikm.komet.kview.mvvm.model.GitHubPreferencesDao; -import dev.ikm.komet.kview.mvvm.view.changeset.ExportController; -import dev.ikm.komet.kview.mvvm.view.changeset.ImportController; -import dev.ikm.komet.kview.mvvm.view.changeset.exchange.*; -import dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode; import dev.ikm.komet.kview.mvvm.view.journal.JournalController; import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController; import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageViewFactory; -import dev.ikm.komet.kview.mvvm.viewmodel.GitHubPreferencesViewModel; -import dev.ikm.komet.list.ListNodeFactory; -import dev.ikm.komet.navigator.graph.GraphNavigatorNodeFactory; -import dev.ikm.komet.navigator.pattern.PatternNavigatorFactory; import dev.ikm.komet.preferences.KometPreferences; import dev.ikm.komet.preferences.KometPreferencesImpl; import dev.ikm.komet.preferences.Preferences; -import dev.ikm.komet.progress.CompletionNodeFactory; -import dev.ikm.komet.progress.ProgressNodeFactory; -import dev.ikm.komet.search.SearchNodeFactory; -import dev.ikm.komet.table.TableNodeFactory; import dev.ikm.tinkar.common.alert.AlertObject; import dev.ikm.tinkar.common.alert.AlertStreams; -import dev.ikm.tinkar.common.binary.Encodable; import dev.ikm.tinkar.common.service.PrimitiveData; -import dev.ikm.tinkar.common.service.ServiceKeys; -import dev.ikm.tinkar.common.service.ServiceProperties; import dev.ikm.tinkar.common.service.TinkExecutor; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; -import javafx.event.ActionEvent; import javafx.fxml.FXMLLoader; -import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.stage.Modality; import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.util.Duration; import one.jpro.platform.auth.core.authentication.User; -import org.carlfx.cognitive.loader.Config; -import org.carlfx.cognitive.loader.FXMLMvvmLoader; -import org.carlfx.cognitive.loader.JFXNode; -import org.eclipse.collections.api.factory.Lists; -import org.eclipse.collections.api.list.ImmutableList; +import one.jpro.platform.utils.CommandRunner; +import one.jpro.platform.utils.PlatformUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.management.ManagementFactory; import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; import java.util.prefs.BackingStoreException; import static dev.ikm.komet.app.AppState.*; +import static dev.ikm.komet.app.LoginFeatureFlag.ENABLED_WEB_ONLY; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; -import static dev.ikm.komet.framework.KometNode.PreferenceKey.CURRENT_JOURNAL_WINDOW_TOPIC; -import static dev.ikm.komet.framework.KometNodeFactory.KOMET_NODES; -import static dev.ikm.komet.framework.events.FrameworkTopics.*; -import static dev.ikm.komet.framework.window.WindowSettings.Keys.*; +import static dev.ikm.komet.framework.events.FrameworkTopics.IMPORT_TOPIC; import static dev.ikm.komet.kview.events.EventTopics.JOURNAL_TOPIC; -import static dev.ikm.komet.kview.events.JournalTileEvent.UPDATE_JOURNAL_TILE; -import static dev.ikm.komet.kview.fxutils.FXUtils.getFocusedWindow; -import static dev.ikm.komet.kview.fxutils.FXUtils.runOnFxThread; -import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitPropertyName.*; -import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.*; -import static dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController.LANDING_PAGE_SOURCE; -import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.VIEW_PROPERTIES; -import static dev.ikm.komet.kview.mvvm.viewmodel.ImportViewModel.ImportField.DESTINATION_TOPIC; -import static dev.ikm.komet.kview.mvvm.viewmodel.JournalViewModel.WINDOW_VIEW; +import static dev.ikm.komet.kview.events.EventTopics.USER_TOPIC; import static dev.ikm.komet.preferences.JournalWindowPreferences.*; import static dev.ikm.komet.preferences.JournalWindowSettings.*; - /** - * JavaFX App + * Main application class for the Komet application, extending JavaFX {@link Application}. + *

+ * The {@code WebApp} class serves as the entry point for launching the Komet application, + * which is a JavaFX-based application supporting both desktop and web platforms via JPro. + * It manages initialization, startup, and shutdown processes, and handles various application states + * such as starting, login, data source selection, running, and shutdown. + *

+ *

+ *

Platform-Specific Features:

+ *
    + *
  • Web Support: Utilizes JPro's {@link WebAPI} to support running the application in a web browser.
  • + *
  • macOS Integration: Configures macOS-specific properties and menus.
  • + *
+ *

+ *

+ *

Event Bus and Subscriptions:

+ * The application uses an event bus ({@link EvtBus}) for inter-component communication. It subscribes to various events like + * {@code CreateJournalEvent} and {@code SignInUserEvent} to handle user actions and state changes. + *

+ * + * @see Application + * @see AppState + * @see LoginFeatureFlag + * @see KometPreferences */ -public class App extends Application implements AppInterface { - - private static final String OS_NAME_MAC = "mac"; - private static final String CHANGESETS_DIR = "changeSets"; +public class App extends Application implements AppInterface { private static final Logger LOG = LoggerFactory.getLogger(App.class); + public static final String ICON_LOCATION = "/icons/Komet.png"; public static final SimpleObjectProperty state = new SimpleObjectProperty<>(STARTING); - private Stage primaryStage; + public static final SimpleObjectProperty userProperty = new SimpleObjectProperty<>(); - private static Stage classicKometStage; + private static Stage primaryStage; - // variables specific to resource overlay - private Stage overlayStage; - private Timeline resourceUsageTimeline; + private static WebAPI webAPI; + static final boolean IS_BROWSER = WebAPI.isBrowser(); + static final boolean IS_DESKTOP = !IS_BROWSER && PlatformUtils.isDesktop(); + static final boolean IS_MAC = !IS_BROWSER && PlatformUtils.isMac(); + static final boolean IS_MAC_AND_NOT_TESTFX_TEST = IS_MAC && !isTestFXTest(); + private final StackPane rootPane = createRootPane(); + private Image appIcon; private LandingPageController landingPageController; private EvtBus kViewEventBus; - private static Stage landingPageWindow; AppGithub appGithub; AppClassicKomet appClassicKomet; @@ -177,19 +139,17 @@ public AppClassicKomet getAppClassicKomet() { @Override public WebAPI getWebAPI() { - return null; + return webAPI; } @Override public Image getAppIcon() { - return null; + return appIcon; } - @Override public Stage getPrimaryStage() { return primaryStage; } - @Override public SimpleObjectProperty getState() { return state; @@ -197,7 +157,7 @@ public SimpleObjectProperty getState() { @Override public StackPane getRootPane() { - return null; + return rootPane; } @Override @@ -220,6 +180,11 @@ public EvtBus getKViewEventBus() { return kViewEventBus; } + /** + * An entry point to launch the newer UI panels. + */ + private MenuItem createJournalViewMenuItem; + /** * This is a list of new windows that have been launched. During shutdown, the application close each stage gracefully. */ @@ -230,215 +195,299 @@ public EvtBus getKViewEventBus() { */ private final GitHubPreferencesDao gitHubPreferencesDao = new GitHubPreferencesDao(); + /** + * Main method that serves as the entry point for the JavaFX application. + * + * @param args Command line arguments for the application. + */ public static void main(String[] args) { + // Launch the JavaFX application + launch(args); + } + + /** + * Configures system properties specific to macOS to ensure proper integration + * with the macOS application menu. + */ + private static void configureMacOSProperties() { + // Ensures that the macOS application menu is used instead of a screen menu bar System.setProperty("apple.laf.useScreenMenuBar", "false"); + + // Sets the name of the application in the macOS application menu System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Komet"); - // https://stackoverflow.com/questions/42598097/using-javafx-application-stop-method-over-shutdownhook + } + + /** + * Adds a shutdown hook to the Java Virtual Machine (JVM) to ensure that data is saved and resources + * are released gracefully before the application exits. + * This method should be called during the application's initialization phase to ensure that the shutdown + * hook is registered before the application begins execution. By doing so, it guarantees that critical + * cleanup operations are performed even if the application is terminated unexpectedly. + */ + private static void addShutdownHook() { + // Adding a shutdown hook that ensures data is saved and resources are released before the application exits Runtime.getRuntime().addShutdownHook(new Thread(() -> { LOG.info("Starting shutdown hook"); - PrimitiveData.save(); - PrimitiveData.stop(); + + try { + // Save and stop primitive data services gracefully + PrimitiveData.save(); + PrimitiveData.stop(); + } catch (Exception e) { + LOG.error("Error during shutdown hook execution", e); + } + LOG.info("Finished shutdown hook"); })); - launch(); } - public void init() { - File logDirectory = new File(System.getProperty("user.home"), "Solor/komet/logs"); - logDirectory.mkdirs(); + @Override + public void init() throws Exception { LOG.info("Starting Komet"); + + // Set system properties for macOS look and feel and application name in the menu bar + configureMacOSProperties(); + + // Add a shutdown hook to ensure resources are saved and properly cleaned up before the application exits + addShutdownHook(); + + // Load custom fonts required by the application LoadFonts.load(); - // get the instance of the event bus + // Load the application icon + appIcon = new Image(App.class.getResource(ICON_LOCATION).toString(), true); + + // Initialize the event bus for inter-component communication kViewEventBus = EvtBusFactory.getInstance(EvtBus.class); + + // Create a subscriber for handling CreateJournalEvent Subscriber detailsSubscriber = evt -> { final PrefX journalWindowSettingsObjectMap = evt.getWindowSettingsObjectMap(); final UUID journalTopic = journalWindowSettingsObjectMap.getValue(JOURNAL_TOPIC); - // Inspects the existing journal windows to see if it is already open - // So that we do not open duplicate journal windows + final String journalName = journalWindowSettingsObjectMap.getValue(JOURNAL_TITLE); + // Check if a journal window with the same title is already open journalControllersList.stream() .filter(journalController -> journalController.getJournalTopic().equals(journalTopic)) .findFirst() - .ifPresentOrElse( - JournalController::windowToFront, /* Window already launched now make window to the front (so user sees window) */ - () -> appPages.launchJournalViewPage(journalWindowSettingsObjectMap) /* launch new Journal view window */ - ); + .ifPresentOrElse(journalController -> { + if (IS_BROWSER) { + // Similar to the desktop version, bring the existing tab to the front + Stage journalStage = (Stage) journalController.getJournalBorderPane().getScene().getWindow(); + webAPI.openStageAsTab(journalStage, journalName.replace(" ", "_")); + } else { + // Bring the existing window to the front + journalController.windowToFront(); + } + }, + () -> appPages.launchJournalViewPage(journalWindowSettingsObjectMap)); }; - // subscribe to the topic + + // Subscribe the subscriber to the JOURNAL_TOPIC kViewEventBus.subscribe(JOURNAL_TOPIC, CreateJournalEvent.class, detailsSubscriber); + + Subscriber signInUserEventSubscriber = evt -> { + final User user = evt.getUser(); + userProperty.set(user); + + if (state.get() == RUNNING) { + launchLandingPage(primaryStage, user); + } else { + state.set(AppState.SELECT_DATA_SOURCE); + state.addListener(this::appStateChangeListener); + appPages.launchSelectDataSourcePage(primaryStage); + } + }; + + // Subscribe the subscriber to the USER_TOPIC + kViewEventBus.subscribe(USER_TOPIC, SignInUserEvent.class, signInUserEventSubscriber); + + //Pops up the import dialog window on any events received on the IMPORT_TOPIC + Subscriber importSubscriber = _ -> { + appMenu.openImport(primaryStage); + }; + kViewEventBus.subscribe(IMPORT_TOPIC, Evt.class, importSubscriber); } @Override public void start(Stage stage) { - - /* appGithub = new AppGithub(this); appClassicKomet = new AppClassicKomet(this); appMenu = new AppMenu(this); appPages = new AppPages(this); try { - primaryStage = stage; - Thread.currentThread().setUncaughtExceptionHandler((t, e) -> AlertStreams.getRoot().dispatch(AlertObject.makeError(e))); - // Get the toolkit - MenuToolkit tk = MenuToolkit.toolkit(); - Menu kometAppMenu = tk.createDefaultApplicationMenu("Komet"); - - MenuItem prefsItem = new MenuItem("Komet preferences..."); - prefsItem.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN)); - prefsItem.setOnAction(event -> appClassicKomet.kometPreferencesStage.showPreferences()); + App.primaryStage = stage; + Thread.currentThread().setUncaughtExceptionHandler((thread, exception) -> + AlertStreams.getRoot().dispatch(AlertObject.makeError(exception))); - kometAppMenu.getItems().add(2, prefsItem); - kometAppMenu.getItems().add(3, new SeparatorMenuItem()); - MenuItem appleQuit = kometAppMenu.getItems().getLast(); - appleQuit.setOnAction(event -> quit()); + // Initialize the JPro WebAPI + if (IS_BROWSER) { + webAPI = WebAPI.getWebAPI(stage); + } - MenuItem appleAbout = kometAppMenu.getItems().getFirst(); - appleAbout.setOnAction(event -> showAboutDialog()); + // Set the application icon + stage.getIcons().setAll(appIcon); - tk.setApplicationMenu(kometAppMenu); + Scene scene = new Scene(rootPane); + addStylesheets(scene, KOMET_CSS, KVIEW_CSS); + stage.setScene(scene); - // File Menu - Menu fileMenu = new Menu("File"); + // Handle the login feature based on the platform and the provided feature flag + handleLoginFeature(ENABLED_WEB_ONLY, stage); + addEventFilters(stage); - MenuItem importDatasetMenuItem = new MenuItem("Import Dataset"); - importDatasetMenuItem.setOnAction(actionEvent -> openImport(primaryStage)); + // This is called only when the user clicks the close button on the window + stage.setOnCloseRequest(windowEvent -> state.set(SHUTDOWN)); - // Exporting data - MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset"); - exportDatasetMenuItem.setOnAction(actionEvent -> openExport(primaryStage)); - fileMenu.getItems().add(exportDatasetMenuItem); + // Show stage and set initial state + stage.show(); + } catch (Exception ex) { + LOG.error("Failed to initialize the application", ex); + Platform.exit(); + } + } - fileMenu.getItems().addAll(importDatasetMenuItem, exportDatasetMenuItem, new SeparatorMenuItem(), tk.createCloseWindowMenuItem()); + /** + * Handles the login feature based on the provided {@link LoginFeatureFlag} and platform. + * + * @param loginFeatureFlag the current state of the login feature + * @param stage the current application stage + */ + public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) { + switch (loginFeatureFlag) { + case ENABLED_WEB_ONLY -> { + if (IS_BROWSER) { + startLogin(stage); + } else { + startSelectDataSource(stage); + } + } + case ENABLED_DESKTOP_ONLY -> { + if (IS_DESKTOP) { + startLogin(stage); + } else { + startSelectDataSource(stage); + } + } + case ENABLED -> startLogin(stage); + case DISABLED -> startSelectDataSource(stage); + } + } - // Edit - Menu editMenu = new Menu("Edit"); - editMenu.getItems().addAll(createMenuItem("Undo"), createMenuItem("Redo"), new SeparatorMenuItem(), - createMenuItem("Cut"), createMenuItem("Copy"), createMenuItem("Paste"), createMenuItem("Select All")); + /** + * Initiates the login process by setting the application state to {@link AppState#LOGIN} + * and launching the login page. + * + * @param stage the current application stage + */ + private void startLogin(Stage stage) { + state.set(LOGIN); + appPages.launchLoginPage(stage); + } - // View - Menu viewMenu = new Menu("View"); - MenuItem classicKometMenuItem = createClassicKometMenuItem(); - MenuItem resourceUsageMenuItem = createResourceUsageItem(); - viewMenu.getItems().addAll(classicKometMenuItem, resourceUsageMenuItem); + /** + * Initiates the data source selection process by setting the application state + * to {@link AppState#SELECT_DATA_SOURCE}, adding a state change listener, and launching + * the data source selection page. + * + * @param stage the current application stage + */ + private void startSelectDataSource(Stage stage) { + state.set(SELECT_DATA_SOURCE); + state.addListener(this::appStateChangeListener); + appPages.launchSelectDataSourcePage(stage); + } - // Window Menu - Menu windowMenu = new Menu("Window"); - windowMenu.getItems().addAll(tk.createMinimizeMenuItem(), tk.createZoomMenuItem(), tk.createCycleWindowsItem(), - new SeparatorMenuItem(), tk.createBringAllToFrontItem()); + @Override + public void stop() { + LOG.info("Stopping application\n\n###############\n\n"); - // Exchange Menu - Menu exchangeMenu = createExchangeMenu(); + appGithub.disconnectFromGithub(); - // Help Menu - Menu helpMenu = new Menu("Help"); - helpMenu.getItems().addAll(new MenuItem("Getting started")); + if (IS_DESKTOP) { + // close all journal windows + journalControllersList.forEach(JournalController::close); + } + } - MenuBar bar = new MenuBar(); - bar.getMenus().addAll(kometAppMenu, fileMenu, editMenu, viewMenu, windowMenu, exchangeMenu, helpMenu); - tk.setAppearanceMode(AppearanceMode.AUTO); - tk.setDockIconMenu(createDockMenu()); - tk.autoAddWindowMenuItems(windowMenu); + private StackPane createRootPane(Node... children) { + return new StackPane(children) { - if (System.getProperty("os.name") != null && - System.getProperty("os.name").toLowerCase().startsWith(OS_NAME_MAC)) { - tk.setGlobalMenuBar(bar); + @Override + protected void layoutChildren() { + if (IS_BROWSER) { + Scene scene = primaryStage.getScene(); + if (scene != null) { + webAPI.layoutRoot(scene); + } + } + super.layoutChildren(); } + }; + } - tk.setTrayMenu(createSampleMenu()); - - FXMLLoader sourceLoader = new FXMLLoader(getClass().getResource("SelectDataSource.fxml")); - BorderPane sourceRoot = sourceLoader.load(); - SelectDataSourceController selectDataSourceController = sourceLoader.getController(); - selectDataSourceController.getCancelButton().setOnAction(actionEvent -> quit()); - Scene sourceScene = new Scene(sourceRoot); - addStylesheets(sourceScene, KOMET_CSS, KVIEW_CSS); - - stage.setScene(sourceScene); - stage.setTitle("KOMET Startup"); - - stage.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { - ScreenInfo.mouseIsPressed(true); - ScreenInfo.mouseWasDragged(false); - }); - stage.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> { - ScreenInfo.mouseIsPressed(false); - ScreenInfo.mouseIsDragging(false); - }); - stage.addEventFilter(MouseEvent.DRAG_DETECTED, event -> { - ScreenInfo.mouseIsDragging(true); - ScreenInfo.mouseWasDragged(true); - }); - - // Ensure app is shutdown gracefully. Once state changes it calls appStateChangeListener. - stage.setOnCloseRequest(windowEvent -> state.set(SHUTDOWN)); - stage.show(); - state.set(AppState.SELECT_DATA_SOURCE); - state.addListener(this::appStateChangeListener); + /** + * Checks if the application is being tested using TestFX Framework. + * + * @return {@code true} if testing mode; {@code false} otherwise. + */ + private static boolean isTestFXTest() { + String testFxTest = System.getProperty("testfx.headless"); + return testFxTest != null && !testFxTest.isBlank(); + } - //Pops up the import dialog window on any events received on the IMPORT_TOPIC - Subscriber importSubscriber = event -> { - openImport(LANDING_PAGE_SOURCE.equals(event.getSource()) ? LANDING_PAGE_TOPIC : PROGRESS_TOPIC); - }; - kViewEventBus.subscribe(IMPORT_TOPIC, Evt.class, importSubscriber); - } catch (IOException e) { - LOG.error(e.getLocalizedMessage(), e); - Platform.exit(); - }*/ + private void addEventFilters(Stage stage) { + stage.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { + ScreenInfo.mouseIsPressed(true); + ScreenInfo.mouseWasDragged(false); + }); + stage.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> { + ScreenInfo.mouseIsPressed(false); + ScreenInfo.mouseIsDragging(false); + }); + stage.addEventFilter(MouseEvent.DRAG_DETECTED, event -> { + ScreenInfo.mouseIsDragging(true); + ScreenInfo.mouseWasDragged(true); + }); } public void launchLandingPage(Stage stage, User user) { - if (landingPageWindow != null) { - primaryStage = landingPageWindow; - landingPageWindow.show(); - landingPageWindow.toFront(); - landingPageWindow.setMaximized(true); - return; - } - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - KometPreferences windowPreferences = appPreferences.node("main-komet-window"); - WindowSettings windowSettings = new WindowSettings(windowPreferences); + try { + rootPane.getChildren().clear(); // Clear the root pane before adding new content - Stage kViewStage = new Stage(); + KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); + KometPreferences windowPreferences = appPreferences.node("main-komet-window"); + WindowSettings windowSettings = new WindowSettings(windowPreferences); - try { FXMLLoader landingPageLoader = LandingPageViewFactory.createFXMLLoader(); BorderPane landingPageBorderPane = landingPageLoader.load(); - // if NOT on Mac OS - if (System.getProperty("os.name") != null && !System.getProperty("os.name").toLowerCase().startsWith(OS_NAME_MAC)) { + + if (!IS_MAC) { appMenu.createMenuOptions(landingPageBorderPane); } + landingPageController = landingPageLoader.getController(); + landingPageController.getWelcomeTitleLabel().setText("Welcome " + user.getName()); landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); landingPageController.getGithubStatusHyperlink().setOnAction(_ -> appGithub.connectToGithub()); - Scene sourceScene = new Scene(landingPageBorderPane, 1200, 800); - kViewStage.setScene(sourceScene); - kViewStage.setTitle("Landing Page"); - - kViewStage.setMaximized(true); - kViewStage.setOnCloseRequest(windowEvent -> { - // call shutdown method on the view + stage.setTitle("Landing Page"); + stage.setMaximized(true); + stage.setOnCloseRequest(windowEvent -> { + // This is called only when the user clicks the close button on the window state.set(SHUTDOWN); landingPageController.cleanup(); - landingPageWindow.close(); - landingPageWindow = null; }); - } catch (IOException e) { - throw new RuntimeException(e); - } - // Launch windows window pane inside journal view - kViewStage.setOnShown(windowEvent -> { - // do stuff when shown. - }); + rootPane.getChildren().add(landingPageBorderPane); - landingPageWindow = kViewStage; - kViewStage.show(); - kViewStage.setMaximized(true); + getAppMenu().setupMenus(); + } catch (IOException e) { + LOG.error("Failed to initialize the landing page window", e); + } } /** @@ -497,16 +546,6 @@ public void saveJournalWindowsToPreferences() { } } - @Override - public void stop() { - LOG.info("Stopping application\n\n###############\n\n"); - - appGithub.disconnectFromGithub(); - - // close all journal windows - Lists.immutable.ofAll(this.journalControllersList).forEach(JournalController::close); - } - private void appStateChangeListener(ObservableValue observable, AppState oldValue, AppState newValue) { try { switch (newValue) { @@ -514,17 +553,18 @@ private void appStateChangeListener(ObservableValue observab Platform.runLater(() -> state.set(LOADING_DATA_SOURCE)); TinkExecutor.threadPool().submit(new LoadDataSourceTask(state)); } - case RUNNING -> { - primaryStage.hide(); - launchLandingPage(primaryStage, null); - } - case SHUTDOWN -> { - quit(); + if (userProperty.get() == null) { + String username = System.getProperty("user.name"); + String capitalizeUsername = username.substring(0, 1).toUpperCase() + username.substring(1); + userProperty.set(new User(capitalizeUsername)); + } + launchLandingPage(primaryStage, userProperty.get()); } + case SHUTDOWN -> quit(); } } catch (Throwable e) { - e.printStackTrace(); + LOG.error("Error during state change", e); Platform.exit(); } } @@ -534,95 +574,48 @@ public void quit() { PrimitiveData.stop(); Preferences.stop(); Platform.exit(); + stopServer(); } /** - * Create a Resource Usage overlay to display metrics - * - * @return The menu item for launching the resource usage overlay. + * Stops the JPro server by running the stop script. */ - private MenuItem createResourceUsageItem() { - MenuItem resourceUsageItem = new MenuItem("Resource Usage"); - KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN); - resourceUsageItem.setOnAction(actionEvent -> { - if (overlayStage != null && overlayStage.isShowing()) { - overlayStage.hide(); + public void stopServer() { + if (IS_BROWSER) { + LOG.info("Stopping JPro server..."); + final String jproMode = WebAPI.getServerInfo().getJProMode(); + final String[] stopScriptArgs; + final CommandRunner stopCommandRunner; + final File workingDir; + if ("dev".equalsIgnoreCase(jproMode)) { + workingDir = new File(System.getProperty("user.dir")).getParentFile(); + stopScriptArgs = PlatformUtils.isWindows() ? + new String[]{"cmd", "/c", "mvnw.bat", "-f", "application", "-Pjpro", "jpro:stop"} : + new String[]{"bash", "./mvnw", "-f", "application", "-Pjpro", "jpro:stop"}; + stopCommandRunner = new CommandRunner(stopScriptArgs); } else { - showResourceUsageOverlay(); + workingDir = new File("bin"); + final String scriptExtension = PlatformUtils.isWindows() ? ".bat" : ".sh"; + final String stopScriptName = "stop" + scriptExtension; + stopScriptArgs = PlatformUtils.isWindows() ? + new String[]{"cmd", "/c", stopScriptName} : new String[]{"bash", stopScriptName}; + stopCommandRunner = new CommandRunner(stopScriptArgs); } - }); - resourceUsageItem.setAccelerator(classicKometKeyCombo); - return resourceUsageItem; - } - - /** - * Show the resource usage overlay. - */ - private void showResourceUsageOverlay() { - overlayStage = new Stage(); - overlayStage.initOwner(getFocusedWindow()); - overlayStage.initModality(Modality.APPLICATION_MODAL); - overlayStage.initStyle(StageStyle.TRANSPARENT); - - VBox overlayContent = new VBox(); - overlayContent.setAlignment(Pos.CENTER); - overlayContent.setStyle("-fx-background-color: rgba(0, 0, 0, 0.5); -fx-padding: 20;"); - - Label cpuUsageLabel = new Label(); - Label memoryUsageLabel = new Label(); - cpuUsageLabel.setTextFill(Color.WHITE); - memoryUsageLabel.setTextFill(Color.WHITE); - overlayContent.getChildren().addAll(cpuUsageLabel, memoryUsageLabel); - - Scene overlayScene = new Scene(overlayContent, 300, 200, Color.TRANSPARENT); - overlayStage.setScene(overlayScene); - - // Set custom close request handler - overlayStage.setOnCloseRequest(event -> { - if (resourceUsageTimeline != null) { - resourceUsageTimeline.stop(); // Stop the timeline + try { + stopCommandRunner.setPrintToConsole(true); + final int exitCode = stopCommandRunner.run("jpro-stop", workingDir); + if (exitCode == 0) { + LOG.info("JPro server stopped successfully."); + } else { + LOG.error("Failed to stop JPro server. Exit code: {}", exitCode); + } + } catch (IOException | InterruptedException ex) { + LOG.error("Error stopping the server", ex); } - overlayStage.hide(); // Hide the stage - }); - overlayStage.show(); - - // Create and start the timeline to update resource usage - resourceUsageTimeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> { - updateResourceUsage(cpuUsageLabel, memoryUsageLabel); - })); - resourceUsageTimeline.setCycleCount(Timeline.INDEFINITE); - resourceUsageTimeline.play(); - } - - /** - * Update the resource usage labels with CPU and memory usage. - * - * @param cpuUsageLabel the label to be used for the CPU usage - * @param memoryUsageLabel the label to be used for the memory usage - */ - private void updateResourceUsage(final Label cpuUsageLabel, final Label memoryUsageLabel) { - java.lang.management.OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); - double cpuLoad = -1; - if (osBean instanceof OperatingSystemMXBean sunOsBean) { - cpuLoad = sunOsBean.getCpuLoad() * 100; - - long totalMemory = Runtime.getRuntime().totalMemory(); - long freeMemory = Runtime.getRuntime().freeMemory(); - long usedMemory = totalMemory - freeMemory; - - cpuUsageLabel.setText("CPU Usage: %.2f%%".formatted(cpuLoad)); - memoryUsageLabel.setText("Memory Usage: %d MB / %d MB".formatted( - usedMemory / (1024 * 1024), totalMemory / (1024 * 1024))); } } public enum AppKeys { APP_INITIALIZED } - - @Override - public void stopServer() { - // No server to stop in this application - LOG.info("No server to stop in this application."); - } -} \ No newline at end of file +} diff --git a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java index 135b8e6f9..57ad699b2 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java +++ b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java @@ -4,7 +4,6 @@ import dev.ikm.komet.framework.KometNode; import dev.ikm.komet.framework.activity.ActivityStreamOption; import dev.ikm.komet.framework.activity.ActivityStreams; -import dev.ikm.komet.framework.events.FrameworkTopics; import dev.ikm.komet.framework.preferences.KometPreferencesStage; import dev.ikm.komet.framework.preferences.Reconstructor; import dev.ikm.komet.framework.tabs.DetachableTab; @@ -12,8 +11,6 @@ import dev.ikm.komet.framework.window.KometStageController; import dev.ikm.komet.framework.window.MainWindowRecord; import dev.ikm.komet.framework.window.WindowComponent; -import dev.ikm.komet.framework.window.WindowSettings; -import dev.ikm.komet.kview.mvvm.view.changeset.ImportController; import dev.ikm.komet.list.ListNodeFactory; import dev.ikm.komet.navigator.graph.GraphNavigatorNodeFactory; import dev.ikm.komet.navigator.pattern.PatternNavigatorFactory; @@ -34,13 +31,7 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; import javafx.stage.Stage; -import javafx.stage.StageStyle; -import org.carlfx.cognitive.loader.Config; -import org.carlfx.cognitive.loader.FXMLMvvmLoader; -import org.carlfx.cognitive.loader.JFXNode; import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.list.ImmutableList; import org.slf4j.Logger; @@ -51,14 +42,11 @@ import java.util.function.Consumer; import java.util.prefs.BackingStoreException; -import static dev.ikm.komet.app.WebApp.*; +import static dev.ikm.komet.app.App.*; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; import static dev.ikm.komet.framework.KometNodeFactory.KOMET_NODES; import static dev.ikm.komet.framework.window.WindowSettings.Keys.*; -import static dev.ikm.komet.kview.fxutils.FXUtils.getFocusedWindow; -import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.VIEW_PROPERTIES; -import static dev.ikm.komet.kview.mvvm.viewmodel.ImportViewModel.ImportField.DESTINATION_TOPIC; import static dev.ikm.komet.preferences.JournalWindowPreferences.JOURNALS; import static dev.ikm.komet.preferences.JournalWindowPreferences.MAIN_KOMET_WINDOW; @@ -95,7 +83,7 @@ void launchClassicKomet() throws IOException, BackingStoreException { //Starting up preferences and getting configurations Preferences.start(); KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - boolean appInitialized = appPreferences.getBoolean(WebApp.AppKeys.APP_INITIALIZED, false); + boolean appInitialized = appPreferences.getBoolean(App.AppKeys.APP_INITIALIZED, false); if (appInitialized) { LOG.info("Restoring configuration preferences."); } else { @@ -128,7 +116,7 @@ void launchClassicKomet() throws IOException, BackingStoreException { controller.setCenterTabs(makeDefaultCenterTabs(controller.windowView()), 0); controller.setRightTabs(makeDefaultRightTabs(controller.windowView()), 1); windowPreferences.putBoolean(KometStageController.WindowKeys.WINDOW_INITIALIZED, true); - appPreferences.putBoolean(WebApp.AppKeys.APP_INITIALIZED, true); + appPreferences.putBoolean(App.AppKeys.APP_INITIALIZED, true); } else { // Restore nodes from preferences. windowPreferences.get(LEFT_TAB_PREFERENCES).ifPresent(leftTabPreferencesName -> diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java index 0bd96bc8b..30c2ce286 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppMenu.java +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -36,8 +36,8 @@ import java.util.prefs.BackingStoreException; import static dev.ikm.komet.app.AppState.RUNNING; -import static dev.ikm.komet.app.WebApp.IS_BROWSER; -import static dev.ikm.komet.app.WebApp.IS_MAC_AND_NOT_TESTFX_TEST; +import static dev.ikm.komet.app.App.IS_BROWSER; +import static dev.ikm.komet.app.App.IS_MAC_AND_NOT_TESTFX_TEST; import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.PULL; import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.SYNC; import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.VIEW_PROPERTIES; diff --git a/application/src/main/java/dev/ikm/komet/app/AppPages.java b/application/src/main/java/dev/ikm/komet/app/AppPages.java index 51077f043..d7536fb38 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppPages.java +++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java @@ -25,8 +25,8 @@ import java.util.Objects; import java.util.UUID; -import static dev.ikm.komet.app.WebApp.IS_BROWSER; -import static dev.ikm.komet.app.WebApp.IS_MAC; +import static dev.ikm.komet.app.App.IS_BROWSER; +import static dev.ikm.komet.app.App.IS_MAC; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; diff --git a/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java b/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java index 43022695b..bdb2026aa 100644 --- a/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java +++ b/application/src/main/java/dev/ikm/komet/app/SelectDataSourceController.java @@ -187,8 +187,6 @@ void okButtonPressed(ActionEvent event) { progressTabPane.getTabs().add(progressTab); App.state.set(AppState.SELECTED_DATA_SOURCE); - // TODO: The following line will be removed in the future, when the WebApp class will be merged with the App class. - WebApp.state.set(AppState.SELECTED_DATA_SOURCE); } private void saveDataServiceProperties(DataServiceController dataServiceController) { From 95776546b61c13a1c4118275900afd665abc165f Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Thu, 7 Aug 2025 22:02:21 +0200 Subject: [PATCH 08/10] Recovered the report usage menu item. --- .../main/java/dev/ikm/komet/app/AppMenu.java | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java index 30c2ce286..7ba244940 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppMenu.java +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -10,30 +10,35 @@ import dev.ikm.komet.kview.mvvm.view.changeset.ImportController; import dev.ikm.komet.preferences.KometPreferences; import dev.ikm.komet.preferences.KometPreferencesImpl; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.event.ActionEvent; +import javafx.geometry.Pos; import javafx.scene.Scene; -import javafx.scene.control.Menu; -import javafx.scene.control.MenuBar; -import javafx.scene.control.MenuItem; -import javafx.scene.control.SeparatorMenuItem; +import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; +import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; +import javafx.util.Duration; import org.carlfx.cognitive.loader.Config; import org.carlfx.cognitive.loader.FXMLMvvmLoader; import org.carlfx.cognitive.loader.JFXNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - +import static dev.ikm.komet.kview.fxutils.FXUtils.getFocusedWindow; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.util.prefs.BackingStoreException; +import com.sun.management.OperatingSystemMXBean; import static dev.ikm.komet.app.AppState.RUNNING; import static dev.ikm.komet.app.App.IS_BROWSER; @@ -51,6 +56,8 @@ public class AppMenu { KometPreferencesStage kometPreferencesStage; private static long windowCount = 1; + private Stage overlayStage; + private Timeline resourceUsageTimeline; public AppMenu(AppInterface app) { this.app = app; @@ -141,7 +148,8 @@ public void createMenuOptions(BorderPane landingPageRoot) { Menu viewMenu = new Menu("View"); MenuItem classicKometMenuItem = createClassicKometMenuItem(); - viewMenu.getItems().add(classicKometMenuItem); + MenuItem resourceUsageMenuItem = createResourceUsageItem(); + viewMenu.getItems().addAll(classicKometMenuItem, resourceUsageMenuItem); Menu windowMenu = new Menu("Window"); MenuItem minimizeWindow = new MenuItem("Minimize"); @@ -389,4 +397,85 @@ public void openExport(Stage owner) { exportStage.show(); } + + /** + * Create a Resource Usage overlay to display metrics + * + * @return The menu item for launching the resource usage overlay. + */ + private MenuItem createResourceUsageItem() { + MenuItem resourceUsageItem = new MenuItem("Resource Usage"); + KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.R, KeyCombination.CONTROL_DOWN); + resourceUsageItem.setOnAction(actionEvent -> { + if (overlayStage != null && overlayStage.isShowing()) { + overlayStage.hide(); + } else { + showResourceUsageOverlay(); + } + }); + resourceUsageItem.setAccelerator(classicKometKeyCombo); + return resourceUsageItem; + } + + /** + * Show the resource usage overlay. + */ + private void showResourceUsageOverlay() { + overlayStage = new Stage(); + overlayStage.initOwner(getFocusedWindow()); + overlayStage.initModality(Modality.APPLICATION_MODAL); + overlayStage.initStyle(StageStyle.TRANSPARENT); + + VBox overlayContent = new VBox(); + overlayContent.setAlignment(Pos.CENTER); + overlayContent.setStyle("-fx-background-color: rgba(0, 0, 0, 0.5); -fx-padding: 20;"); + + Label cpuUsageLabel = new Label(); + Label memoryUsageLabel = new Label(); + cpuUsageLabel.setTextFill(Color.WHITE); + memoryUsageLabel.setTextFill(Color.WHITE); + overlayContent.getChildren().addAll(cpuUsageLabel, memoryUsageLabel); + + Scene overlayScene = new Scene(overlayContent, 300, 200, Color.TRANSPARENT); + overlayStage.setScene(overlayScene); + + // Set custom close request handler + overlayStage.setOnCloseRequest(event -> { + if (resourceUsageTimeline != null) { + resourceUsageTimeline.stop(); // Stop the timeline + } + overlayStage.hide(); // Hide the stage + }); + overlayStage.show(); + + // Create and start the timeline to update resource usage + resourceUsageTimeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> { + updateResourceUsage(cpuUsageLabel, memoryUsageLabel); + })); + resourceUsageTimeline.setCycleCount(Timeline.INDEFINITE); + resourceUsageTimeline.play(); + } + + /** + * Update the resource usage labels with CPU and memory usage. + * + * @param cpuUsageLabel the label to be used for the CPU usage + * @param memoryUsageLabel the label to be used for the memory usage + */ + private void updateResourceUsage(final Label cpuUsageLabel, final Label memoryUsageLabel) { + java.lang.management.OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + double cpuLoad = -1; + if (osBean instanceof OperatingSystemMXBean sunOsBean) { + cpuLoad = sunOsBean.getCpuLoad() * 100; + + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + long usedMemory = totalMemory - freeMemory; + + cpuUsageLabel.setText("CPU Usage: %.2f%%".formatted(cpuLoad)); + memoryUsageLabel.setText("Memory Usage: %d MB / %d MB".formatted( + usedMemory / (1024 * 1024), totalMemory / (1024 * 1024))); + } + } + } From 3016f2a9dd52bd53bf64b2e9c455bc1de73c75ea Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Fri, 8 Aug 2025 11:40:26 +0200 Subject: [PATCH 09/10] removed the AppInterface and simplified the logic --- .../src/main/java/dev/ikm/komet/app/App.java | 40 +- .../dev/ikm/komet/app/AppClassicKomet.java | 4 +- .../java/dev/ikm/komet/app/AppGithub.java | 4 +- .../java/dev/ikm/komet/app/AppInterface.java | 73 -- .../main/java/dev/ikm/komet/app/AppMenu.java | 6 +- .../main/java/dev/ikm/komet/app/AppPages.java | 5 +- .../main/java/dev/ikm/komet/app/WebApp.java | 683 ------------------ 7 files changed, 24 insertions(+), 791 deletions(-) delete mode 100644 application/src/main/java/dev/ikm/komet/app/AppInterface.java delete mode 100644 application/src/main/java/dev/ikm/komet/app/WebApp.java diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index d67f4b22c..b668edc75 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -98,7 +98,7 @@ * @see LoginFeatureFlag * @see KometPreferences */ -public class App extends Application implements AppInterface { +public class App extends Application { private static final Logger LOG = LoggerFactory.getLogger(App.class); public static final String ICON_LOCATION = "/icons/Komet.png"; @@ -122,61 +122,51 @@ public class App extends Application implements AppInterface { AppMenu appMenu; AppPages appPages; - @Override - public AppGithub getAppGithub() { + AppGithub getAppGithub() { return appGithub; } - @Override - public AppMenu getAppMenu() { + AppMenu getAppMenu() { return appMenu; } - @Override - public AppClassicKomet getAppClassicKomet() { + AppClassicKomet getAppClassicKomet() { return appClassicKomet; } - @Override - public WebAPI getWebAPI() { + WebAPI getWebAPI() { return webAPI; } - @Override - public Image getAppIcon() { + Image getAppIcon() { return appIcon; } - @Override - public Stage getPrimaryStage() { + + Stage getPrimaryStage() { return primaryStage; } - @Override - public SimpleObjectProperty getState() { + + SimpleObjectProperty getStateProperty() { return state; } - @Override - public StackPane getRootPane() { + StackPane getRootPane() { return rootPane; } - @Override - public LandingPageController getLandingPageController() { + LandingPageController getLandingPageController() { return landingPageController; } - @Override - public GitHubPreferencesDao getGitHubPreferencesDao() { + GitHubPreferencesDao getGitHubPreferencesDao() { return gitHubPreferencesDao; } - @Override - public List getJournalControllersList() { + List getJournalControllersList() { return journalControllersList; } - @Override - public EvtBus getKViewEventBus() { + EvtBus getKViewEventBus() { return kViewEventBus; } diff --git a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java index 57ad699b2..8af32f1b9 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java +++ b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java @@ -56,14 +56,14 @@ public class AppClassicKomet { private Stage classicKometStage; - private final AppInterface app; + private final App app; /** * An entry point to launch the newer UI panels. */ private MenuItem createJournalViewMenuItem; - public AppClassicKomet(AppInterface app) { + public AppClassicKomet(App app) { this.app = app; } diff --git a/application/src/main/java/dev/ikm/komet/app/AppGithub.java b/application/src/main/java/dev/ikm/komet/app/AppGithub.java index a6ab07945..a7dd032e0 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppGithub.java +++ b/application/src/main/java/dev/ikm/komet/app/AppGithub.java @@ -31,9 +31,9 @@ public class AppGithub { private static final Logger LOG = LoggerFactory.getLogger(AppGithub.class); static final String CHANGESETS_DIR = "changeSets"; - private final AppInterface app; + private final App app; - public AppGithub(AppInterface app) { + public AppGithub(App app) { this.app = app; } diff --git a/application/src/main/java/dev/ikm/komet/app/AppInterface.java b/application/src/main/java/dev/ikm/komet/app/AppInterface.java deleted file mode 100644 index 6f09e3870..000000000 --- a/application/src/main/java/dev/ikm/komet/app/AppInterface.java +++ /dev/null @@ -1,73 +0,0 @@ -package dev.ikm.komet.app; - -import com.jpro.webapi.WebAPI; -import dev.ikm.komet.framework.events.EvtBus; -import dev.ikm.komet.kview.mvvm.model.GitHubPreferencesDao; -import dev.ikm.komet.kview.mvvm.view.journal.JournalController; -import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController; -import javafx.beans.property.SimpleObjectProperty; -import javafx.scene.image.Image; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; -import one.jpro.platform.auth.core.authentication.User; - -import java.util.List; - -/** - * Contains the shared elements between App and WebApp. - * Ideally, this will be removed and only one App is left - because they become the same. - */ -public interface AppInterface { - - - AppMenu getAppMenu(); - - AppGithub getAppGithub(); - - AppClassicKomet getAppClassicKomet(); - - /** - * Returns the default App icon. - * - * @return the {@link Image} representing the app icon - */ - Image getAppIcon(); - - WebAPI getWebAPI(); - - - SimpleObjectProperty getState(); - - /** - * Returns the controller for the landing page. - * - * @return the {@link LandingPageController} instance - */ - LandingPageController getLandingPageController(); - - /** - * Returns the GitHub preferences DAO. - * - * @return the {@link GitHubPreferencesDao} instance - */ - GitHubPreferencesDao getGitHubPreferencesDao(); - - void saveJournalWindowsToPreferences(); - - List getJournalControllersList(); - - /** - * Quits the application. - */ - void quit(); - - void stopServer(); - - StackPane getRootPane(); - - void launchLandingPage(Stage stage, User user); - - Stage getPrimaryStage(); - - EvtBus getKViewEventBus(); -} diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java index 7ba244940..5e02b9cb1 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppMenu.java +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -52,14 +52,14 @@ public class AppMenu { private static final Logger LOG = LoggerFactory.getLogger(AppMenu.class); - private final AppInterface app; + private final App app; KometPreferencesStage kometPreferencesStage; private static long windowCount = 1; private Stage overlayStage; private Timeline resourceUsageTimeline; - public AppMenu(AppInterface app) { + public AppMenu(App app) { this.app = app; } @@ -213,7 +213,7 @@ void setupMenus() { MenuBar menuBar = new MenuBar(kometAppMenu); - if (app.getState().get() == RUNNING) { + if (app.getStateProperty().get() == RUNNING) { Menu fileMenu = createFileMenu(); Menu editMenu = createEditMenu(); Menu viewMenu = createViewMenu(); diff --git a/application/src/main/java/dev/ikm/komet/app/AppPages.java b/application/src/main/java/dev/ikm/komet/app/AppPages.java index d7536fb38..187e4b9b5 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppPages.java +++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java @@ -41,13 +41,12 @@ public class AppPages { private static final Logger LOG = LoggerFactory.getLogger(AppGithub.class); - private final AppInterface app; + private final App app; - public AppPages(AppInterface app) { + public AppPages(App app) { this.app = app; } - void launchLoginPage(Stage stage) { JFXNode loginNode = FXMLMvvmLoader.make( LoginPageController.class.getResource("login-page.fxml")); diff --git a/application/src/main/java/dev/ikm/komet/app/WebApp.java b/application/src/main/java/dev/ikm/komet/app/WebApp.java deleted file mode 100644 index d21ca0d24..000000000 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ /dev/null @@ -1,683 +0,0 @@ -/* - * Copyright © 2015 Integrated Knowledge Management (support@ikm.dev) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.ikm.komet.app; - -import com.jpro.webapi.WebAPI; -import de.jangassen.MenuToolkit; -import de.jangassen.model.AppearanceMode; -import dev.ikm.komet.app.aboutdialog.AboutDialog; -import dev.ikm.komet.details.DetailsNodeFactory; -import dev.ikm.komet.framework.KometNode; -import dev.ikm.komet.framework.KometNodeFactory; -import dev.ikm.komet.framework.ScreenInfo; -import dev.ikm.komet.framework.activity.ActivityStreamOption; -import dev.ikm.komet.framework.activity.ActivityStreams; -import dev.ikm.komet.framework.events.Evt; -import dev.ikm.komet.framework.events.EvtBus; -import dev.ikm.komet.framework.events.EvtBusFactory; -import dev.ikm.komet.framework.events.Subscriber; -import dev.ikm.komet.framework.graphics.Icon; -import dev.ikm.komet.framework.graphics.LoadFonts; -import dev.ikm.komet.framework.preferences.KometPreferencesStage; -import dev.ikm.komet.framework.preferences.PrefX; -import dev.ikm.komet.framework.preferences.Reconstructor; -import dev.ikm.komet.framework.progress.ProgressHelper; -import dev.ikm.komet.framework.tabs.DetachableTab; -import dev.ikm.komet.framework.view.ObservableViewNoOverride; -import dev.ikm.komet.framework.window.KometStageController; -import dev.ikm.komet.framework.window.MainWindowRecord; -import dev.ikm.komet.framework.window.WindowComponent; -import dev.ikm.komet.framework.window.WindowSettings; -import dev.ikm.komet.kview.controls.GlassPane; -import dev.ikm.komet.kview.events.CreateJournalEvent; -import dev.ikm.komet.kview.events.JournalTileEvent; -import dev.ikm.komet.kview.events.SignInUserEvent; -import dev.ikm.komet.kview.mvvm.model.GitHubPreferencesDao; -import dev.ikm.komet.kview.mvvm.view.changeset.ExportController; -import dev.ikm.komet.kview.mvvm.view.changeset.ImportController; -import dev.ikm.komet.kview.mvvm.view.changeset.exchange.*; -import dev.ikm.komet.kview.mvvm.view.journal.JournalController; -import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageController; -import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageViewFactory; -import dev.ikm.komet.kview.mvvm.view.login.LoginPageController; -import dev.ikm.komet.kview.mvvm.viewmodel.GitHubPreferencesViewModel; -import dev.ikm.komet.list.ListNodeFactory; -import dev.ikm.komet.navigator.graph.GraphNavigatorNodeFactory; -import dev.ikm.komet.navigator.pattern.PatternNavigatorFactory; -import dev.ikm.komet.preferences.KometPreferences; -import dev.ikm.komet.preferences.KometPreferencesImpl; -import dev.ikm.komet.preferences.Preferences; -import dev.ikm.komet.progress.CompletionNodeFactory; -import dev.ikm.komet.progress.ProgressNodeFactory; -import dev.ikm.komet.search.SearchNodeFactory; -import dev.ikm.komet.table.TableNodeFactory; -import dev.ikm.tinkar.common.alert.AlertObject; -import dev.ikm.tinkar.common.alert.AlertStreams; -import dev.ikm.tinkar.common.binary.Encodable; -import dev.ikm.tinkar.common.service.PrimitiveData; -import dev.ikm.tinkar.common.service.ServiceKeys; -import dev.ikm.tinkar.common.service.ServiceProperties; -import dev.ikm.tinkar.common.service.TinkExecutor; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.event.ActionEvent; -import javafx.fxml.FXMLLoader; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.stage.Window; -import one.jpro.platform.auth.core.authentication.User; -import one.jpro.platform.utils.CommandRunner; -import one.jpro.platform.utils.PlatformUtils; -import org.carlfx.cognitive.loader.Config; -import org.carlfx.cognitive.loader.FXMLMvvmLoader; -import org.carlfx.cognitive.loader.JFXNode; -import org.eclipse.collections.api.factory.Lists; -import org.eclipse.collections.api.list.ImmutableList; -import org.eclipse.jgit.util.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; -import java.util.prefs.BackingStoreException; - -import static dev.ikm.komet.app.AppState.*; -import static dev.ikm.komet.app.LoginFeatureFlag.ENABLED_WEB_ONLY; -import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; -import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; -import static dev.ikm.komet.app.util.CssUtils.addStylesheets; -import static dev.ikm.komet.framework.KometNodeFactory.KOMET_NODES; -import static dev.ikm.komet.framework.events.FrameworkTopics.IMPORT_TOPIC; -import static dev.ikm.komet.framework.events.FrameworkTopics.LANDING_PAGE_TOPIC; -import static dev.ikm.komet.framework.window.WindowSettings.Keys.*; -import static dev.ikm.komet.kview.events.EventTopics.JOURNAL_TOPIC; -import static dev.ikm.komet.kview.events.EventTopics.USER_TOPIC; -import static dev.ikm.komet.kview.events.JournalTileEvent.UPDATE_JOURNAL_TILE; -import static dev.ikm.komet.kview.fxutils.FXUtils.runOnFxThread; -import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitPropertyName.*; -import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.OperationMode.*; -import static dev.ikm.komet.kview.mvvm.view.changeset.exchange.GitTask.README_FILENAME; -import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.CURRENT_JOURNAL_WINDOW_TOPIC; -import static dev.ikm.komet.kview.mvvm.viewmodel.FormViewModel.VIEW_PROPERTIES; -import static dev.ikm.komet.kview.mvvm.viewmodel.JournalViewModel.WINDOW_VIEW; -import static dev.ikm.komet.preferences.JournalWindowPreferences.*; -import static dev.ikm.komet.preferences.JournalWindowSettings.*; - -/** - * Main application class for the Komet application, extending JavaFX {@link Application}. - *

- * The {@code WebApp} class serves as the entry point for launching the Komet application, - * which is a JavaFX-based application supporting both desktop and web platforms via JPro. - * It manages initialization, startup, and shutdown processes, and handles various application states - * such as starting, login, data source selection, running, and shutdown. - *

- *

- *

Platform-Specific Features:

- *
    - *
  • Web Support: Utilizes JPro's {@link WebAPI} to support running the application in a web browser.
  • - *
  • macOS Integration: Configures macOS-specific properties and menus.
  • - *
- *

- *

- *

Event Bus and Subscriptions:

- * The application uses an event bus ({@link EvtBus}) for inter-component communication. It subscribes to various events like - * {@code CreateJournalEvent} and {@code SignInUserEvent} to handle user actions and state changes. - *

- * - * @see Application - * @see AppState - * @see LoginFeatureFlag - * @see KometPreferences - */ -public class WebApp extends Application implements AppInterface { - - private static final Logger LOG = LoggerFactory.getLogger(WebApp.class); - public static final String ICON_LOCATION = "/icons/Komet.png"; - public static final SimpleObjectProperty state = new SimpleObjectProperty<>(STARTING); - public static final SimpleObjectProperty userProperty = new SimpleObjectProperty<>(); - - private static Stage primaryStage; - - private static WebAPI webAPI; - static final boolean IS_BROWSER = WebAPI.isBrowser(); - static final boolean IS_DESKTOP = !IS_BROWSER && PlatformUtils.isDesktop(); - static final boolean IS_MAC = !IS_BROWSER && PlatformUtils.isMac(); - static final boolean IS_MAC_AND_NOT_TESTFX_TEST = IS_MAC && !isTestFXTest(); - private final StackPane rootPane = createRootPane(); - private Image appIcon; - private LandingPageController landingPageController; - private EvtBus kViewEventBus; - - AppGithub appGithub; - AppClassicKomet appClassicKomet; - AppMenu appMenu; - AppPages appPages; - - @Override - public AppGithub getAppGithub() { - return appGithub; - } - - @Override - public AppMenu getAppMenu() { - return appMenu; - } - - @Override - public AppClassicKomet getAppClassicKomet() { - return appClassicKomet; - } - - @Override - public WebAPI getWebAPI() { - return webAPI; - } - - @Override - public Image getAppIcon() { - return appIcon; - } - @Override - public Stage getPrimaryStage() { - return primaryStage; - } - @Override - public SimpleObjectProperty getState() { - return state; - } - - @Override - public StackPane getRootPane() { - return rootPane; - } - - @Override - public LandingPageController getLandingPageController() { - return landingPageController; - } - - @Override - public GitHubPreferencesDao getGitHubPreferencesDao() { - return gitHubPreferencesDao; - } - - @Override - public List getJournalControllersList() { - return journalControllersList; - } - - @Override - public EvtBus getKViewEventBus() { - return kViewEventBus; - } - - /** - * An entry point to launch the newer UI panels. - */ - private MenuItem createJournalViewMenuItem; - - /** - * This is a list of new windows that have been launched. During shutdown, the application close each stage gracefully. - */ - private final List journalControllersList = new ArrayList<>(); - - /** - * GitHub preferences data access object. - */ - private final GitHubPreferencesDao gitHubPreferencesDao = new GitHubPreferencesDao(); - - /** - * Main method that serves as the entry point for the JavaFX application. - * - * @param args Command line arguments for the application. - */ - public static void main(String[] args) { - // Launch the JavaFX application - launch(args); - } - - /** - * Configures system properties specific to macOS to ensure proper integration - * with the macOS application menu. - */ - private static void configureMacOSProperties() { - // Ensures that the macOS application menu is used instead of a screen menu bar - System.setProperty("apple.laf.useScreenMenuBar", "false"); - - // Sets the name of the application in the macOS application menu - System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Komet"); - } - - /** - * Adds a shutdown hook to the Java Virtual Machine (JVM) to ensure that data is saved and resources - * are released gracefully before the application exits. - * This method should be called during the application's initialization phase to ensure that the shutdown - * hook is registered before the application begins execution. By doing so, it guarantees that critical - * cleanup operations are performed even if the application is terminated unexpectedly. - */ - private static void addShutdownHook() { - // Adding a shutdown hook that ensures data is saved and resources are released before the application exits - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - LOG.info("Starting shutdown hook"); - - try { - // Save and stop primitive data services gracefully - PrimitiveData.save(); - PrimitiveData.stop(); - } catch (Exception e) { - LOG.error("Error during shutdown hook execution", e); - } - - LOG.info("Finished shutdown hook"); - })); - } - - @Override - public void init() throws Exception { - LOG.info("Starting Komet"); - - // Set system properties for macOS look and feel and application name in the menu bar - configureMacOSProperties(); - - // Add a shutdown hook to ensure resources are saved and properly cleaned up before the application exits - addShutdownHook(); - - // Load custom fonts required by the application - LoadFonts.load(); - - // Load the application icon - appIcon = new Image(WebApp.class.getResource(ICON_LOCATION).toString(), true); - - // Initialize the event bus for inter-component communication - kViewEventBus = EvtBusFactory.getInstance(EvtBus.class); - - // Create a subscriber for handling CreateJournalEvent - Subscriber detailsSubscriber = evt -> { - final PrefX journalWindowSettingsObjectMap = evt.getWindowSettingsObjectMap(); - final UUID journalTopic = journalWindowSettingsObjectMap.getValue(JOURNAL_TOPIC); - final String journalName = journalWindowSettingsObjectMap.getValue(JOURNAL_TITLE); - // Check if a journal window with the same title is already open - journalControllersList.stream() - .filter(journalController -> - journalController.getJournalTopic().equals(journalTopic)) - .findFirst() - .ifPresentOrElse(journalController -> { - if (IS_BROWSER) { - // Similar to the desktop version, bring the existing tab to the front - Stage journalStage = (Stage) journalController.getJournalBorderPane().getScene().getWindow(); - webAPI.openStageAsTab(journalStage, journalName.replace(" ", "_")); - } else { - // Bring the existing window to the front - journalController.windowToFront(); - } - }, - () -> appPages.launchJournalViewPage(journalWindowSettingsObjectMap)); - }; - - // Subscribe the subscriber to the JOURNAL_TOPIC - kViewEventBus.subscribe(JOURNAL_TOPIC, CreateJournalEvent.class, detailsSubscriber); - - Subscriber signInUserEventSubscriber = evt -> { - final User user = evt.getUser(); - userProperty.set(user); - - if (state.get() == RUNNING) { - launchLandingPage(primaryStage, user); - } else { - state.set(AppState.SELECT_DATA_SOURCE); - state.addListener(this::appStateChangeListener); - appPages.launchSelectDataSourcePage(primaryStage); - } - }; - - // Subscribe the subscriber to the USER_TOPIC - kViewEventBus.subscribe(USER_TOPIC, SignInUserEvent.class, signInUserEventSubscriber); - - //Pops up the import dialog window on any events received on the IMPORT_TOPIC - Subscriber importSubscriber = _ -> { - appMenu.openImport(primaryStage); - }; - kViewEventBus.subscribe(IMPORT_TOPIC, Evt.class, importSubscriber); - } - - @Override - public void start(Stage stage) { - appGithub = new AppGithub(this); - appClassicKomet = new AppClassicKomet(this); - appMenu = new AppMenu(this); - appPages = new AppPages(this); - - try { - WebApp.primaryStage = stage; - Thread.currentThread().setUncaughtExceptionHandler((thread, exception) -> - AlertStreams.getRoot().dispatch(AlertObject.makeError(exception))); - - // Initialize the JPro WebAPI - if (IS_BROWSER) { - webAPI = WebAPI.getWebAPI(stage); - } - - // Set the application icon - stage.getIcons().setAll(appIcon); - - Scene scene = new Scene(rootPane); - addStylesheets(scene, KOMET_CSS, KVIEW_CSS); - stage.setScene(scene); - - // Handle the login feature based on the platform and the provided feature flag - handleLoginFeature(ENABLED_WEB_ONLY, stage); - - addEventFilters(stage); - - // This is called only when the user clicks the close button on the window - stage.setOnCloseRequest(windowEvent -> state.set(SHUTDOWN)); - - // Show stage and set initial state - stage.show(); - } catch (Exception ex) { - LOG.error("Failed to initialize the application", ex); - Platform.exit(); - } - } - - /** - * Handles the login feature based on the provided {@link LoginFeatureFlag} and platform. - * - * @param loginFeatureFlag the current state of the login feature - * @param stage the current application stage - */ - public void handleLoginFeature(LoginFeatureFlag loginFeatureFlag, Stage stage) { - switch (loginFeatureFlag) { - case ENABLED_WEB_ONLY -> { - if (IS_BROWSER) { - startLogin(stage); - } else { - startSelectDataSource(stage); - } - } - case ENABLED_DESKTOP_ONLY -> { - if (IS_DESKTOP) { - startLogin(stage); - } else { - startSelectDataSource(stage); - } - } - case ENABLED -> startLogin(stage); - case DISABLED -> startSelectDataSource(stage); - } - } - - /** - * Initiates the login process by setting the application state to {@link AppState#LOGIN} - * and launching the login page. - * - * @param stage the current application stage - */ - private void startLogin(Stage stage) { - state.set(LOGIN); - appPages.launchLoginPage(stage); - } - - /** - * Initiates the data source selection process by setting the application state - * to {@link AppState#SELECT_DATA_SOURCE}, adding a state change listener, and launching - * the data source selection page. - * - * @param stage the current application stage - */ - private void startSelectDataSource(Stage stage) { - state.set(SELECT_DATA_SOURCE); - state.addListener(this::appStateChangeListener); - appPages.launchSelectDataSourcePage(stage); - } - - @Override - public void stop() { - LOG.info("Stopping application\n\n###############\n\n"); - - appGithub.disconnectFromGithub(); - - if (IS_DESKTOP) { - // close all journal windows - journalControllersList.forEach(JournalController::close); - } - } - - private StackPane createRootPane(Node... children) { - return new StackPane(children) { - - @Override - protected void layoutChildren() { - if (IS_BROWSER) { - Scene scene = primaryStage.getScene(); - if (scene != null) { - webAPI.layoutRoot(scene); - } - } - super.layoutChildren(); - } - }; - } - - /** - * Checks if the application is being tested using TestFX Framework. - * - * @return {@code true} if testing mode; {@code false} otherwise. - */ - private static boolean isTestFXTest() { - String testFxTest = System.getProperty("testfx.headless"); - return testFxTest != null && !testFxTest.isBlank(); - } - - - private void addEventFilters(Stage stage) { - stage.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { - ScreenInfo.mouseIsPressed(true); - ScreenInfo.mouseWasDragged(false); - }); - stage.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> { - ScreenInfo.mouseIsPressed(false); - ScreenInfo.mouseIsDragging(false); - }); - stage.addEventFilter(MouseEvent.DRAG_DETECTED, event -> { - ScreenInfo.mouseIsDragging(true); - ScreenInfo.mouseWasDragged(true); - }); - } - - public void launchLandingPage(Stage stage, User user) { - try { - rootPane.getChildren().clear(); // Clear the root pane before adding new content - - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - KometPreferences windowPreferences = appPreferences.node("main-komet-window"); - WindowSettings windowSettings = new WindowSettings(windowPreferences); - - FXMLLoader landingPageLoader = LandingPageViewFactory.createFXMLLoader(); - BorderPane landingPageBorderPane = landingPageLoader.load(); - - if (!IS_MAC) { - appMenu.createMenuOptions(landingPageBorderPane); - } - - landingPageController = landingPageLoader.getController(); - landingPageController.getWelcomeTitleLabel().setText("Welcome " + user.getName()); - landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); - landingPageController.getGithubStatusHyperlink().setOnAction(_ -> appGithub.connectToGithub()); - - stage.setTitle("Landing Page"); - stage.setMaximized(true); - stage.setOnCloseRequest(windowEvent -> { - // This is called only when the user clicks the close button on the window - state.set(SHUTDOWN); - landingPageController.cleanup(); - }); - - rootPane.getChildren().add(landingPageBorderPane); - - getAppMenu().setupMenus(); - } catch (IOException e) { - LOG.error("Failed to initialize the landing page window", e); - } - } - - /** - * Saves the current state of the journals and its windows to the application's preferences system. - *

- * This method persists all journal-related data, including: - *

    - *
  • All open window states (via {@link JournalController#saveWindows(KometPreferences)})
  • - *
  • Journal metadata (topic UUID, title, directory name)
  • - *
  • Window geometry (width, height, x/y position)
  • - *
  • Author information
  • - *
  • Last edit timestamp
  • - *
- *

- * The preferences are stored in a hierarchical structure: - *

-     * Root Configuration Preferences
-     *   └── journals
-     *       └── [journal_shortened-UUID]
-     *           ├── Journal metadata (UUID, title, dimensions, position, etc.)
-     *           └── Window states
-     * 
- */ - public void saveJournalWindowsToPreferences() { - final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - final KometPreferences journalsPreferences = appPreferences.node(JOURNALS); - - // Non launched journal windows should be preserved. - List journalSubWindowFoldersFromPref = journalsPreferences.getList(JOURNAL_IDS); - - // launched (journal Controllers List) will overwrite existing window preferences. - List journalSubWindowFolders = new ArrayList<>(journalControllersList.size()); - for (JournalController controller : journalControllersList) { - String journalSubWindowPrefFolder = controller.getJournalDirName(); - journalSubWindowFolders.add(journalSubWindowPrefFolder); - - final KometPreferences journalSubWindowPreferences = appPreferences.node(JOURNALS + - File.separator + journalSubWindowPrefFolder); - controller.saveWindows(journalSubWindowPreferences); - } - - // Make sure windows that are not summoned will not be deleted (not added to JOURNAL_NAMES) - for (String x : journalSubWindowFolders) { - if (!journalSubWindowFoldersFromPref.contains(x)) { - journalSubWindowFoldersFromPref.add(x); - } - } - journalsPreferences.putList(JOURNAL_IDS, journalSubWindowFoldersFromPref); - - try { - journalsPreferences.flush(); - appPreferences.flush(); - appPreferences.sync(); - } catch (BackingStoreException e) { - LOG.error("error writing journal window flag to preferences", e); - } - } - - private void appStateChangeListener(ObservableValue observable, AppState oldValue, AppState newValue) { - try { - switch (newValue) { - case SELECTED_DATA_SOURCE -> { - Platform.runLater(() -> state.set(LOADING_DATA_SOURCE)); - TinkExecutor.threadPool().submit(new LoadDataSourceTask(state)); - } - case RUNNING -> { - if (userProperty.get() == null) { - String username = System.getProperty("user.name"); - String capitalizeUsername = username.substring(0, 1).toUpperCase() + username.substring(1); - userProperty.set(new User(capitalizeUsername)); - } - launchLandingPage(primaryStage, userProperty.get()); - } - case SHUTDOWN -> quit(); - } - } catch (Throwable e) { - LOG.error("Error during state change", e); - Platform.exit(); - } - } - - public void quit() { - saveJournalWindowsToPreferences(); - PrimitiveData.stop(); - Preferences.stop(); - Platform.exit(); - stopServer(); - } - - /** - * Stops the JPro server by running the stop script. - */ - public void stopServer() { - if (IS_BROWSER) { - LOG.info("Stopping JPro server..."); - final String jproMode = WebAPI.getServerInfo().getJProMode(); - final String[] stopScriptArgs; - final CommandRunner stopCommandRunner; - final File workingDir; - if ("dev".equalsIgnoreCase(jproMode)) { - workingDir = new File(System.getProperty("user.dir")).getParentFile(); - stopScriptArgs = PlatformUtils.isWindows() ? - new String[]{"cmd", "/c", "mvnw.bat", "-f", "application", "-Pjpro", "jpro:stop"} : - new String[]{"bash", "./mvnw", "-f", "application", "-Pjpro", "jpro:stop"}; - stopCommandRunner = new CommandRunner(stopScriptArgs); - } else { - workingDir = new File("bin"); - final String scriptExtension = PlatformUtils.isWindows() ? ".bat" : ".sh"; - final String stopScriptName = "stop" + scriptExtension; - stopScriptArgs = PlatformUtils.isWindows() ? - new String[]{"cmd", "/c", stopScriptName} : new String[]{"bash", stopScriptName}; - stopCommandRunner = new CommandRunner(stopScriptArgs); - } - try { - stopCommandRunner.setPrintToConsole(true); - final int exitCode = stopCommandRunner.run("jpro-stop", workingDir); - if (exitCode == 0) { - LOG.info("JPro server stopped successfully."); - } else { - LOG.error("Failed to stop JPro server. Exit code: {}", exitCode); - } - } catch (IOException | InterruptedException ex) { - LOG.error("Error stopping the server", ex); - } - } - } - - public enum AppKeys { - APP_INITIALIZED - } -} From 6e18152f4e5f56c07f440437a06f22d36eb4aa1c Mon Sep 17 00:00:00 2001 From: floriankirmaier Date: Fri, 8 Aug 2025 12:11:22 +0200 Subject: [PATCH 10/10] more simplifications. --- .../src/main/java/dev/ikm/komet/app/App.java | 103 ++---------------- .../dev/ikm/komet/app/AppClassicKomet.java | 8 +- .../java/dev/ikm/komet/app/AppGithub.java | 16 +-- .../main/java/dev/ikm/komet/app/AppMenu.java | 18 +-- .../main/java/dev/ikm/komet/app/AppPages.java | 62 +++++++++-- 5 files changed, 82 insertions(+), 125 deletions(-) diff --git a/application/src/main/java/dev/ikm/komet/app/App.java b/application/src/main/java/dev/ikm/komet/app/App.java index b668edc75..9eb6f3da8 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -105,71 +105,23 @@ public class App extends Application { public static final SimpleObjectProperty state = new SimpleObjectProperty<>(STARTING); public static final SimpleObjectProperty userProperty = new SimpleObjectProperty<>(); - private static Stage primaryStage; + static Stage primaryStage; - private static WebAPI webAPI; + WebAPI webAPI; static final boolean IS_BROWSER = WebAPI.isBrowser(); static final boolean IS_DESKTOP = !IS_BROWSER && PlatformUtils.isDesktop(); static final boolean IS_MAC = !IS_BROWSER && PlatformUtils.isMac(); static final boolean IS_MAC_AND_NOT_TESTFX_TEST = IS_MAC && !isTestFXTest(); - private final StackPane rootPane = createRootPane(); - private Image appIcon; - private LandingPageController landingPageController; - private EvtBus kViewEventBus; + final StackPane rootPane = createRootPane(); + Image appIcon; + LandingPageController landingPageController; + EvtBus kViewEventBus; AppGithub appGithub; AppClassicKomet appClassicKomet; AppMenu appMenu; AppPages appPages; - AppGithub getAppGithub() { - return appGithub; - } - - AppMenu getAppMenu() { - return appMenu; - } - - AppClassicKomet getAppClassicKomet() { - return appClassicKomet; - } - - WebAPI getWebAPI() { - return webAPI; - } - - Image getAppIcon() { - return appIcon; - } - - Stage getPrimaryStage() { - return primaryStage; - } - - SimpleObjectProperty getStateProperty() { - return state; - } - - StackPane getRootPane() { - return rootPane; - } - - LandingPageController getLandingPageController() { - return landingPageController; - } - - GitHubPreferencesDao getGitHubPreferencesDao() { - return gitHubPreferencesDao; - } - - List getJournalControllersList() { - return journalControllersList; - } - - EvtBus getKViewEventBus() { - return kViewEventBus; - } - /** * An entry point to launch the newer UI panels. */ @@ -178,12 +130,12 @@ EvtBus getKViewEventBus() { /** * This is a list of new windows that have been launched. During shutdown, the application close each stage gracefully. */ - private final List journalControllersList = new ArrayList<>(); + final List journalControllersList = new ArrayList<>(); /** * GitHub preferences data access object. */ - private final GitHubPreferencesDao gitHubPreferencesDao = new GitHubPreferencesDao(); + final GitHubPreferencesDao gitHubPreferencesDao = new GitHubPreferencesDao(); /** * Main method that serves as the entry point for the JavaFX application. @@ -281,7 +233,7 @@ public void init() throws Exception { userProperty.set(user); if (state.get() == RUNNING) { - launchLandingPage(primaryStage, user); + appPages.launchLandingPage(primaryStage, user); } else { state.set(AppState.SELECT_DATA_SOURCE); state.addListener(this::appStateChangeListener); @@ -444,41 +396,6 @@ private void addEventFilters(Stage stage) { }); } - public void launchLandingPage(Stage stage, User user) { - try { - rootPane.getChildren().clear(); // Clear the root pane before adding new content - - KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); - KometPreferences windowPreferences = appPreferences.node("main-komet-window"); - WindowSettings windowSettings = new WindowSettings(windowPreferences); - - FXMLLoader landingPageLoader = LandingPageViewFactory.createFXMLLoader(); - BorderPane landingPageBorderPane = landingPageLoader.load(); - - if (!IS_MAC) { - appMenu.createMenuOptions(landingPageBorderPane); - } - - landingPageController = landingPageLoader.getController(); - landingPageController.getWelcomeTitleLabel().setText("Welcome " + user.getName()); - landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); - landingPageController.getGithubStatusHyperlink().setOnAction(_ -> appGithub.connectToGithub()); - - stage.setTitle("Landing Page"); - stage.setMaximized(true); - stage.setOnCloseRequest(windowEvent -> { - // This is called only when the user clicks the close button on the window - state.set(SHUTDOWN); - landingPageController.cleanup(); - }); - - rootPane.getChildren().add(landingPageBorderPane); - - getAppMenu().setupMenus(); - } catch (IOException e) { - LOG.error("Failed to initialize the landing page window", e); - } - } /** * Saves the current state of the journals and its windows to the application's preferences system. @@ -549,7 +466,7 @@ private void appStateChangeListener(ObservableValue observab String capitalizeUsername = username.substring(0, 1).toUpperCase() + username.substring(1); userProperty.set(new User(capitalizeUsername)); } - launchLandingPage(primaryStage, userProperty.get()); + appPages.launchLandingPage(primaryStage, userProperty.get()); } case SHUTDOWN -> quit(); } diff --git a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java index 8af32f1b9..cf1a482dd 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java +++ b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java @@ -78,7 +78,7 @@ void launchClassicKomet() throws IOException, BackingStoreException { } classicKometStage = new Stage(); - classicKometStage.getIcons().setAll(app.getAppIcon()); + classicKometStage.getIcons().setAll(app.appIcon); //Starting up preferences and getting configurations Preferences.start(); @@ -101,7 +101,7 @@ void launchClassicKomet() throws IOException, BackingStoreException { // if NOT on macOS if (!IS_MAC) { - app.getAppMenu().generateMsWindowsMenu(kometRoot, classicKometStage); + app.appMenu.generateMsWindowsMenu(kometRoot, classicKometStage); } classicKometStage.setScene(kometScene); @@ -137,10 +137,10 @@ void launchClassicKomet() throws IOException, BackingStoreException { classicKometStage.show(); if (IS_BROWSER) { - app.getWebAPI().openStageAsTab(classicKometStage); + app.webAPI.openStageAsTab(classicKometStage); } - app.getAppMenu().kometPreferencesStage = new KometPreferencesStage(controller.windowView().makeOverridableViewProperties()); + app.appMenu.kometPreferencesStage = new KometPreferencesStage(controller.windowView().makeOverridableViewProperties()); windowPreferences.sync(); appPreferences.sync(); diff --git a/application/src/main/java/dev/ikm/komet/app/AppGithub.java b/application/src/main/java/dev/ikm/komet/app/AppGithub.java index a7dd032e0..429732587 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppGithub.java +++ b/application/src/main/java/dev/ikm/komet/app/AppGithub.java @@ -54,7 +54,7 @@ CompletableFuture promptForGitHubPrefs() { // Show dialog on JavaFX thread runOnFxThread(() -> { - GlassPane glassPane = new GlassPane(app.getLandingPageController().getRoot()); + GlassPane glassPane = new GlassPane(app.landingPageController.getRoot()); final JFXNode githubPreferencesNode = FXMLMvvmLoader .make(GitHubPreferencesController.class.getResource("github-preferences.fxml")); @@ -99,8 +99,8 @@ CompletableFuture promptForGitHubPrefs() { */ private void gotoGitHubDisconnectedState() { runOnFxThread(() -> { - if (app.getLandingPageController() != null) { - Hyperlink githubStatusHyperlink = app.getLandingPageController().getGithubStatusHyperlink(); + if (app.landingPageController != null) { + Hyperlink githubStatusHyperlink = app.landingPageController.getGithubStatusHyperlink(); githubStatusHyperlink.setText("Disconnected, Select to connect"); githubStatusHyperlink.setOnAction(event -> connectToGithub()); } @@ -119,8 +119,8 @@ private void gotoGitHubDisconnectedState() { */ private void gotoGitHubConnectedState() { runOnFxThread(() -> { - if (app.getLandingPageController() != null) { - Hyperlink githubStatusHyperlink = app.getLandingPageController().getGithubStatusHyperlink(); + if (app.landingPageController != null) { + Hyperlink githubStatusHyperlink = app.landingPageController.getGithubStatusHyperlink(); githubStatusHyperlink.setText("Connected"); githubStatusHyperlink.setOnAction(event -> disconnectFromGithub()); } @@ -159,7 +159,7 @@ void executeGitTask(GitTask.OperationMode mode) { } // Check if GitHub preferences are valid first - if (!app.getGitHubPreferencesDao().validate()) { + if (!app.gitHubPreferencesDao.validate()) { LOG.info("GitHub preferences missing or incomplete. Prompting user..."); // Prompt for preferences before proceeding @@ -246,7 +246,7 @@ void disconnectFromGithub() { // Delete stored user preferences related to GitHub try { - app.getGitHubPreferencesDao().delete(); + app.gitHubPreferencesDao.delete(); LOG.info("Successfully deleted GitHub preferences"); } catch (BackingStoreException e) { LOG.error("Failed to delete GitHub preferences", e); @@ -382,7 +382,7 @@ private CompletableFuture showRepositoryInfoDialog(Map { - GlassPane glassPane = new GlassPane(app.getLandingPageController().getRoot()); + GlassPane glassPane = new GlassPane(app.landingPageController.getRoot()); final JFXNode githubInfoNode = FXMLMvvmLoader .make(GitHubInfoController.class.getResource("github-info.fxml")); diff --git a/application/src/main/java/dev/ikm/komet/app/AppMenu.java b/application/src/main/java/dev/ikm/komet/app/AppMenu.java index 5e02b9cb1..1e4613fcf 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppMenu.java +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -90,7 +90,7 @@ void generateMsWindowsMenu(BorderPane kometRoot, Stage stage) { Menu editMenu = new Menu("Edit"); MenuItem landingPage = new MenuItem("Landing Page"); KeyCombination landingPageKeyCombo = new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN); - landingPage.setOnAction(actionEvent -> app.launchLandingPage(app.getPrimaryStage(), null /* userProperty.get()*/)); + landingPage.setOnAction(actionEvent -> app.appPages.launchLandingPage(App.primaryStage, null /* userProperty.get()*/)); landingPage.setAccelerator(landingPageKeyCombo); landingPage.setDisable(IS_BROWSER); editMenu.getItems().add(landingPage); @@ -117,11 +117,11 @@ Menu createExchangeMenu() { Menu exchangeMenu = new Menu("Exchange"); MenuItem infoMenuItem = new MenuItem("Info"); - infoMenuItem.setOnAction(actionEvent -> app.getAppGithub().infoAction()); + infoMenuItem.setOnAction(actionEvent -> app.appGithub.infoAction()); MenuItem pullMenuItem = new MenuItem("Pull"); - pullMenuItem.setOnAction(actionEvent -> app.getAppGithub().executeGitTask(PULL)); + pullMenuItem.setOnAction(actionEvent -> app.appGithub.executeGitTask(PULL)); MenuItem pushMenuItem = new MenuItem("Sync"); - pushMenuItem.setOnAction(actionEvent -> app.getAppGithub().executeGitTask(SYNC)); + pushMenuItem.setOnAction(actionEvent -> app.appGithub.executeGitTask(SYNC)); exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); return exchangeMenu; @@ -176,7 +176,7 @@ private MenuItem createClassicKometMenuItem() { KeyCombination classicKometKeyCombo = new KeyCodeCombination(KeyCode.K, KeyCombination.CONTROL_DOWN); classicKometMenuItem.setOnAction(actionEvent -> { try { - app.getAppClassicKomet().launchClassicKomet(); + app.appClassicKomet.launchClassicKomet(); } catch (IOException e) { throw new RuntimeException(e); } catch (BackingStoreException e) { @@ -213,7 +213,7 @@ void setupMenus() { MenuBar menuBar = new MenuBar(kometAppMenu); - if (app.getStateProperty().get() == RUNNING) { + if (App.state.get() == RUNNING) { Menu fileMenu = createFileMenu(); Menu editMenu = createEditMenu(); Menu viewMenu = createViewMenu(); @@ -248,11 +248,11 @@ private Menu createFileMenu() { // Import Dataset Menu Item MenuItem importDatasetMenuItem = new MenuItem("Import Dataset..."); - importDatasetMenuItem.setOnAction(actionEvent -> openImport(app.getPrimaryStage())); + importDatasetMenuItem.setOnAction(actionEvent -> openImport(App.primaryStage)); // Export Dataset Menu Item MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset..."); - exportDatasetMenuItem.setOnAction(actionEvent -> openExport(app.getPrimaryStage())); + exportDatasetMenuItem.setOnAction(actionEvent -> openExport(App.primaryStage)); // Add menu items to the File menu fileMenu.getItems().addAll(importDatasetMenuItem, exportDatasetMenuItem); @@ -286,7 +286,7 @@ private Menu createViewMenu() { classicKometMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.K, KeyCombination.SHORTCUT_DOWN)); classicKometMenuItem.setOnAction(actionEvent -> { try { - app.getAppClassicKomet().launchClassicKomet(); + app.appClassicKomet.launchClassicKomet(); } catch (IOException | BackingStoreException e) { throw new RuntimeException(e); } diff --git a/application/src/main/java/dev/ikm/komet/app/AppPages.java b/application/src/main/java/dev/ikm/komet/app/AppPages.java index 187e4b9b5..a50f557ab 100644 --- a/application/src/main/java/dev/ikm/komet/app/AppPages.java +++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java @@ -5,16 +5,19 @@ import dev.ikm.komet.framework.window.WindowSettings; import dev.ikm.komet.kview.events.JournalTileEvent; import dev.ikm.komet.kview.mvvm.view.journal.JournalController; +import dev.ikm.komet.kview.mvvm.view.landingpage.LandingPageViewFactory; import dev.ikm.komet.kview.mvvm.view.login.LoginPageController; import dev.ikm.komet.navigator.graph.GraphNavigatorNodeFactory; import dev.ikm.komet.preferences.KometPreferences; import dev.ikm.komet.preferences.KometPreferencesImpl; import dev.ikm.komet.search.SearchNodeFactory; +import dev.ikm.tinkar.common.service.PrimitiveData; import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.stage.Stage; +import one.jpro.platform.auth.core.authentication.User; import org.carlfx.cognitive.loader.Config; import org.carlfx.cognitive.loader.FXMLMvvmLoader; import org.carlfx.cognitive.loader.JFXNode; @@ -27,6 +30,7 @@ import static dev.ikm.komet.app.App.IS_BROWSER; import static dev.ikm.komet.app.App.IS_MAC; +import static dev.ikm.komet.app.AppState.SHUTDOWN; import static dev.ikm.komet.app.util.CssFile.KOMET_CSS; import static dev.ikm.komet.app.util.CssFile.KVIEW_CSS; import static dev.ikm.komet.app.util.CssUtils.addStylesheets; @@ -51,10 +55,10 @@ void launchLoginPage(Stage stage) { JFXNode loginNode = FXMLMvvmLoader.make( LoginPageController.class.getResource("login-page.fxml")); BorderPane loginPane = loginNode.node(); - app.getRootPane().getChildren().setAll(loginPane); + app.rootPane.getChildren().setAll(loginPane); stage.setTitle("KOMET Login"); - app.getAppMenu().setupMenus(); + app.appMenu.setupMenus(); } void launchSelectDataSourcePage(Stage stage) { @@ -67,15 +71,51 @@ void launchSelectDataSourcePage(Stage stage) { Platform.exit(); app.stopServer(); }); - app.getRootPane().getChildren().setAll(sourceRoot); + app.rootPane.getChildren().setAll(sourceRoot); stage.setTitle("KOMET Startup"); - app.getAppMenu().setupMenus(); + app.appMenu.setupMenus(); } catch (IOException ex) { LOG.error("Failed to initialize the select data source window", ex); } } + public void launchLandingPage(Stage stage, User user) { + try { + app.rootPane.getChildren().clear(); // Clear the root pane before adding new content + + KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); + KometPreferences windowPreferences = appPreferences.node("main-komet-window"); + WindowSettings windowSettings = new WindowSettings(windowPreferences); + + FXMLLoader landingPageLoader = LandingPageViewFactory.createFXMLLoader(); + BorderPane landingPageBorderPane = landingPageLoader.load(); + + if (!IS_MAC) { + app.appMenu.createMenuOptions(landingPageBorderPane); + } + + app.landingPageController = landingPageLoader.getController(); + app.landingPageController.getWelcomeTitleLabel().setText("Welcome " + user.getName()); + app.landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); + app.landingPageController.getGithubStatusHyperlink().setOnAction(_ -> app.appGithub.connectToGithub()); + + stage.setTitle("Landing Page"); + stage.setMaximized(true); + stage.setOnCloseRequest(windowEvent -> { + // This is called only when the user clicks the close button on the window + App.state.set(SHUTDOWN); + app.landingPageController.cleanup(); + }); + + app.rootPane.getChildren().add(landingPageBorderPane); + + app.appMenu.setupMenus(); + } catch (IOException e) { + LOG.error("Failed to initialize the landing page window", e); + } + } + /** * When a user selects the menu option View/New Journal a new Stage Window is launched. @@ -105,11 +145,11 @@ void launchJournalViewPage(PrefX journalWindowSettings) { addStylesheets(sourceScene, KOMET_CSS, KVIEW_CSS); Stage journalStage = new Stage(); - journalStage.getIcons().setAll(app.getAppIcon()); + journalStage.getIcons().setAll(app.appIcon); journalStage.setScene(sourceScene); if (!IS_MAC) { - app.getAppMenu().generateMsWindowsMenu(journalBorderPane, journalStage); + app.appMenu.generateMsWindowsMenu(journalBorderPane, journalStage); } // load journal specific window settings @@ -138,10 +178,10 @@ void launchJournalViewPage(PrefX journalWindowSettings) { journalStage.setOnHidden(windowEvent -> { app.saveJournalWindowsToPreferences(); journalController.shutdown(); - app.getJournalControllersList().remove(journalController); + app.journalControllersList.remove(journalController); journalWindowSettings.setValue(CAN_DELETE, true); - app.getKViewEventBus().publish(JOURNAL_TOPIC, + app.kViewEventBus.publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); }); @@ -159,11 +199,11 @@ void launchJournalViewPage(PrefX journalWindowSettings) { }); // disable the delete menu option for a Journal Card. journalWindowSettings.setValue(CAN_DELETE, false); - app.getKViewEventBus().publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); - app.getJournalControllersList().add(journalController); + app.kViewEventBus.publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); + app.journalControllersList.add(journalController); if (IS_BROWSER) { - app.getWebAPI().openStageAsTab(journalStage, journalName.replace(" ", "_")); + app.webAPI.openStageAsTab(journalStage, journalName.replace(" ", "_")); } else { journalStage.show(); }