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 4d267feb8..9eb6f3da8 100644 --- a/application/src/main/java/dev/ikm/komet/app/App.java +++ b/application/src/main/java/dev/ikm/komet/app/App.java @@ -15,145 +15,112 @@ */ package dev.ikm.komet.app; -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 com.jpro.webapi.WebAPI; 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.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; +import javafx.scene.image.Image; 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 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.auth.core.authentication.User; +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:

+ * + *

+ *

+ *

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 { - - private static final String OS_NAME_MAC = "mac"; - private static final String CHANGESETS_DIR = "changeSets"; +public class App extends Application { 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 static Stage primaryStage; + public static final SimpleObjectProperty userProperty = new SimpleObjectProperty<>(); - private static Stage classicKometStage; - private static long windowCount = 1; - private static KometPreferencesStage kometPreferencesStage; + static Stage primaryStage; - // variables specific to resource overlay - private Stage overlayStage; - private Timeline resourceUsageTimeline; - private LandingPageController landingPageController; - private EvtBus kViewEventBus; - private static Stage landingPageWindow; + 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(); + final StackPane rootPane = createRootPane(); + Image appIcon; + LandingPageController landingPageController; + EvtBus kViewEventBus; + AppGithub appGithub; + AppClassicKomet appClassicKomet; + AppMenu appMenu; + AppPages appPages; /** * An entry point to launch the newer UI panels. @@ -163,382 +130,273 @@ public class App extends Application { /** * 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. + * + * @param args Command line arguments for the application. + */ public static void main(String[] args) { - System.setProperty("apple.laf.useScreenMenuBar", "false"); - System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Komet"); - // https://stackoverflow.com/questions/42598097/using-javafx-application-stop-method-over-shutdownhook - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - LOG.info("Starting shutdown hook"); - PrimitiveData.save(); - PrimitiveData.stop(); - LOG.info("Finished shutdown hook"); - })); - launch(); - } - - private static void createNewStage() { - Stage stage = new Stage(); - stage.setScene(new Scene(new StackPane())); - stage.setTitle("New stage" + " " + (windowCount++)); - stage.show(); + // Launch the JavaFX application + launch(args); } - 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); + /** + * 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"); - return Lists.immutable.of(navigatorNode1Tab, patternNavigatorNode1Tab); + // Sets the name of the application in the macOS application menu + System.setProperty("com.apple.mrj.application.apple.menu.about.name", "Komet"); } - 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); + /** + * 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"); - TableNodeFactory tableNodeFactory = new TableNodeFactory(); - KometNode tableNode = tableNodeFactory.create(windowView, - ActivityStreams.UNLINKED, ActivityStreamOption.PUBLISH.keyForOption(), AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab tableNodeTab = new DetachableTab(tableNode); + try { + // Save and stop primitive data services gracefully + PrimitiveData.save(); + PrimitiveData.stop(); + } catch (Exception e) { + LOG.error("Error during shutdown hook execution", e); + } - return Lists.immutable.of(detailsNode1Tab, detailsNode2Tab, detailsNode3Tab, listNodeNodeTab, tableNodeTab); + LOG.info("Finished shutdown hook"); + })); } - 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); + @Override + public void init() throws Exception { + LOG.info("Starting Komet"); - CompletionNodeFactory completionNodeFactory = new CompletionNodeFactory(); - KometNode completionNode = completionNodeFactory.create(windowView, - null, null, AlertStreams.ROOT_ALERT_STREAM_KEY); - DetachableTab completionTab = new DetachableTab(completionNode); + // Set system properties for macOS look and feel and application name in the menu bar + configureMacOSProperties(); - return Lists.immutable.of(newSearchTab, progressTab, completionTab); - } + // Add a shutdown hook to ensure resources are saved and properly cleaned up before the application exits + addShutdownHook(); - public void init() { - File logDirectory = new File(System.getProperty("user.home"), "Solor/komet/logs"); - logDirectory.mkdirs(); - LOG.info("Starting Komet"); + // 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) */ - () -> launchJournalViewWindow(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) { + appPages.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 { App.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 -> App.kometPreferencesStage.showPreferences()); - - kometAppMenu.getItems().add(2, prefsItem); - kometAppMenu.getItems().add(3, new SeparatorMenuItem()); - MenuItem appleQuit = kometAppMenu.getItems().getLast(); - appleQuit.setOnAction(event -> quit()); - - MenuItem appleAbout = kometAppMenu.getItems().getFirst(); - appleAbout.setOnAction(event -> showAboutDialog()); - - tk.setApplicationMenu(kometAppMenu); - - // File Menu - Menu fileMenu = new Menu("File"); - - MenuItem importDatasetMenuItem = new MenuItem("Import Dataset"); - importDatasetMenuItem.setOnAction(actionEvent -> openImport()); - - // Exporting data - MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset"); - exportDatasetMenuItem.setOnAction(actionEvent -> openExport()); - fileMenu.getItems().add(exportDatasetMenuItem); - - fileMenu.getItems().addAll(importDatasetMenuItem, exportDatasetMenuItem, new SeparatorMenuItem(), tk.createCloseWindowMenuItem()); + Thread.currentThread().setUncaughtExceptionHandler((thread, exception) -> + AlertStreams.getRoot().dispatch(AlertObject.makeError(exception))); - // Edit - Menu editMenu = new Menu("Edit"); - editMenu.getItems().addAll(createMenuItem("Undo"), createMenuItem("Redo"), new SeparatorMenuItem(), - createMenuItem("Cut"), createMenuItem("Copy"), createMenuItem("Paste"), createMenuItem("Select All")); - - // View - Menu viewMenu = new Menu("View"); - MenuItem classicKometMenuItem = createClassicKometMenuItem(); - MenuItem resourceUsageMenuItem = createResourceUsageItem(); - viewMenu.getItems().addAll(classicKometMenuItem, resourceUsageMenuItem); - - // Window Menu - Menu windowMenu = new Menu("Window"); - windowMenu.getItems().addAll(tk.createMinimizeMenuItem(), tk.createZoomMenuItem(), tk.createCycleWindowsItem(), - new SeparatorMenuItem(), tk.createBringAllToFrontItem()); - - // Exchange Menu - Menu exchangeMenu = createExchangeMenu(); - - // Help Menu - Menu helpMenu = new Menu("Help"); - helpMenu.getItems().addAll(new MenuItem("Getting started")); - - MenuBar bar = new MenuBar(); - bar.getMenus().addAll(kometAppMenu, fileMenu, editMenu, viewMenu, windowMenu, exchangeMenu, helpMenu); - tk.setAppearanceMode(AppearanceMode.AUTO); - tk.setDockIconMenu(createDockMenu()); - tk.autoAddWindowMenuItems(windowMenu); - - if (System.getProperty("os.name") != null && - System.getProperty("os.name").toLowerCase().startsWith(OS_NAME_MAC)) { - tk.setGlobalMenuBar(bar); + // Initialize the JPro WebAPI + if (IS_BROWSER) { + webAPI = WebAPI.getWebAPI(stage); } - tk.setTrayMenu(createSampleMenu()); + // Set the application icon + stage.getIcons().setAll(appIcon); - 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); + Scene scene = new Scene(rootPane); + addStylesheets(scene, KOMET_CSS, KVIEW_CSS); + stage.setScene(scene); - stage.setScene(sourceScene); - stage.setTitle("KOMET Startup"); + // Handle the login feature based on the platform and the provided feature flag + handleLoginFeature(ENABLED_WEB_ONLY, 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); - }); + addEventFilters(stage); - // Ensure app is shutdown gracefully. Once state changes it calls appStateChangeListener. + // This is called only when the user clicks the close button on the window stage.setOnCloseRequest(windowEvent -> state.set(SHUTDOWN)); - stage.show(); - state.set(AppState.SELECT_DATA_SOURCE); - state.addListener(this::appStateChangeListener); - - //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); + // Show stage and set initial state + stage.show(); + } catch (Exception ex) { + LOG.error("Failed to initialize the application", ex); Platform.exit(); } } - private void launchLandingPage() { - if (landingPageWindow != null) { - App.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); - - Stage kViewStage = new Stage(); - - 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)) { - createMenuOptions(landingPageBorderPane); + /** + * 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); + } } - landingPageController = landingPageLoader.getController(); - landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); - landingPageController.getGithubStatusHyperlink().setOnAction(_ -> 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 - state.set(SHUTDOWN); - landingPageController.cleanup(); - landingPageWindow.close(); - landingPageWindow = null; - }); - } catch (IOException e) { - throw new RuntimeException(e); + case ENABLED_DESKTOP_ONLY -> { + if (IS_DESKTOP) { + startLogin(stage); + } else { + startSelectDataSource(stage); + } + } + case ENABLED -> startLogin(stage); + case DISABLED -> startSelectDataSource(stage); } + } - // Launch windows window pane inside journal view - kViewStage.setOnShown(windowEvent -> { - // do stuff when shown. - }); - - landingPageWindow = kViewStage; - kViewStage.show(); - kViewStage.setMaximized(true); + /** + * 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); } /** - * 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. + * 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 journalWindowSettings if present will give the size and positioning of the journal window + * @param stage the current application stage */ - private void launchJournalViewWindow(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"); + private void startSelectDataSource(Stage stage) { + state.set(SELECT_DATA_SOURCE); + state.addListener(this::appStateChangeListener); + appPages.launchSelectDataSourcePage(stage); + } - // 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); + @Override + public void stop() { + LOG.info("Stopping application\n\n###############\n\n"); - 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); + appGithub.disconnectFromGithub(); + + if (IS_DESKTOP) { + // close all journal windows + journalControllersList.forEach(JournalController::close); } + } - // load journal specific window settings - final String journalName = journalWindowSettings.getValue(JOURNAL_TITLE); - journalStageWindow.setTitle(journalName); + private StackPane createRootPane(Node... children) { + return new StackPane(children) { - // Get the UUID-based directory name from preferences - String journalDirName = journalWindowSettings.getValue(JOURNAL_DIR_NAME); + @Override + protected void layoutChildren() { + if (IS_BROWSER) { + Scene scene = primaryStage.getScene(); + if (scene != null) { + webAPI.layoutRoot(scene); + } + } + super.layoutChildren(); + } + }; + } - // 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); - } + /** + * 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(); + } - 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)); + private void addEventFilters(Stage stage) { + stage.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { + ScreenInfo.mouseIsPressed(true); + ScreenInfo.mouseWasDragged(false); }); - - // 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(); + stage.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> { + ScreenInfo.mouseIsPressed(false); + ScreenInfo.mouseIsDragging(false); + }); + stage.addEventFilter(MouseEvent.DRAG_DETECTED, event -> { + ScreenInfo.mouseIsDragging(true); + ScreenInfo.mouseWasDragged(true); }); - // 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. *

@@ -560,7 +418,7 @@ private void launchJournalViewWindow(PrefX journalWindowSettings) { * └── Window states * */ - private void saveJournalWindowsToPreferences() { + public void saveJournalWindowsToPreferences() { final KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); final KometPreferences journalsPreferences = appPreferences.node(JOURNALS); @@ -595,53 +453,6 @@ private void saveJournalWindowsToPreferences() { } } - @Override - public void stop() { - LOG.info("Stopping application\n\n###############\n\n"); - - disconnectFromGithub(); - - // close all journal windows - 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) { @@ -649,753 +460,69 @@ private void appStateChangeListener(ObservableValue observab Platform.runLater(() -> state.set(LOADING_DATA_SOURCE)); TinkExecutor.threadPool().submit(new LoadDataSourceTask(state)); } - case RUNNING -> { - primaryStage.hide(); - launchLandingPage(); - } - 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)); + } + appPages.launchLandingPage(primaryStage, userProperty.get()); } + case SHUTDOWN -> quit(); } } catch (Throwable e) { - e.printStackTrace(); + LOG.error("Error during state change", e); Platform.exit(); } } - 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); - 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(); - } - - private void openImport() { - openImport(PROGRESS_TOPIC); - } - - private void openExport() { - 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(); - } - - /** - * 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. - *

- * 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 - 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(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; - } - - private void generateMsWindowsMenu(Node node) { - MenuBar menuBar = new MenuBar(); - Menu fileMenu = new Menu("File"); - - MenuItem about = new MenuItem("About"); - about.setOnAction(actionEvent -> 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() { - 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 -> executeGitTask(PULL)); - MenuItem pushMenuItem = new MenuItem("Sync"); - pushMenuItem.setOnAction(actionEvent -> executeGitTask(SYNC)); - - exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); - 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)); - } - }); + stopServer(); } /** - * Create the menu options for the landing page. - * - * @param landingPageRoot The root pane of the landing page. + * Stops the JPro server by running the stop script. */ - 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 { - 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 - * - * @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(); + 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 } -} \ 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 new file mode 100644 index 000000000..cf1a482dd --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppClassicKomet.java @@ -0,0 +1,240 @@ +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.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.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.stage.Stage; +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.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.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 App app; + + /** + * An entry point to launch the newer UI panels. + */ + private MenuItem createJournalViewMenuItem; + + public AppClassicKomet(App 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.appIcon); + + //Starting up preferences and getting configurations + Preferences.start(); + KometPreferences appPreferences = KometPreferencesImpl.getConfigurationRootPreferences(); + boolean appInitialized = appPreferences.getBoolean(App.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.appMenu.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(App.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.webAPI.openStageAsTab(classicKometStage); + } + + app.appMenu.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/AppGithub.java b/application/src/main/java/dev/ikm/komet/app/AppGithub.java new file mode 100644 index 000000000..429732587 --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppGithub.java @@ -0,0 +1,412 @@ +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.*; +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; +import org.carlfx.cognitive.loader.JFXNode; +import org.slf4j.Logger; +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 { + + private static final Logger LOG = LoggerFactory.getLogger(AppGithub.class); + static final String CHANGESETS_DIR = "changeSets"; + + private final App app; + + public AppGithub(App 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.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 (app.landingPageController != null) { + Hyperlink githubStatusHyperlink = app.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 (app.landingPageController != null) { + Hyperlink githubStatusHyperlink = app.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 + */ + 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.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 + */ + 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.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. + *

+ * 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.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; + } + + + +} 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..1e4613fcf --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppMenu.java @@ -0,0 +1,481 @@ +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 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.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.*; +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; +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; +import static dev.ikm.komet.preferences.JournalWindowPreferences.MAIN_KOMET_WINDOW; + +public class AppMenu { + + private static final Logger LOG = LoggerFactory.getLogger(AppMenu.class); + + private final App app; + KometPreferencesStage kometPreferencesStage; + + private static long windowCount = 1; + private Stage overlayStage; + private Timeline resourceUsageTimeline; + + public AppMenu(App 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(_ -> 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 -> 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.appPages.launchLandingPage(App.primaryStage, 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)); + } + + Menu createExchangeMenu() { + Menu exchangeMenu = new Menu("Exchange"); + + MenuItem infoMenuItem = new MenuItem("Info"); + infoMenuItem.setOnAction(actionEvent -> app.appGithub.infoAction()); + MenuItem pullMenuItem = new MenuItem("Pull"); + pullMenuItem.setOnAction(actionEvent -> app.appGithub.executeGitTask(PULL)); + MenuItem pushMenuItem = new MenuItem("Sync"); + pushMenuItem.setOnAction(actionEvent -> app.appGithub.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(); + 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); + 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.appClassicKomet.launchClassicKomet(); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (BackingStoreException e) { + throw new RuntimeException(e); + } + }); + 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.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 = 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(App.primaryStage)); + + // Export Dataset Menu Item + MenuItem exportDatasetMenuItem = new MenuItem("Export Dataset..."); + exportDatasetMenuItem.setOnAction(actionEvent -> openExport(App.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 { + app.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 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(); + } + + + 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(); + } + + + /** + * 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))); + } + } + +} 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..a50f557ab --- /dev/null +++ b/application/src/main/java/dev/ikm/komet/app/AppPages.java @@ -0,0 +1,211 @@ +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.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; +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.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; +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 App app; + + public AppPages(App app) { + this.app = app; + } + + void launchLoginPage(Stage stage) { + JFXNode loginNode = FXMLMvvmLoader.make( + LoginPageController.class.getResource("login-page.fxml")); + BorderPane loginPane = loginNode.node(); + app.rootPane.getChildren().setAll(loginPane); + stage.setTitle("KOMET Login"); + + app.appMenu.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.rootPane.getChildren().setAll(sourceRoot); + stage.setTitle("KOMET Startup"); + + 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. + * 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.appIcon); + journalStage.setScene(sourceScene); + + if (!IS_MAC) { + app.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 -> { + app.saveJournalWindowsToPreferences(); + journalController.shutdown(); + app.journalControllersList.remove(journalController); + + journalWindowSettings.setValue(CAN_DELETE, true); + app.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); + app.kViewEventBus.publish(JOURNAL_TOPIC, new JournalTileEvent(this, UPDATE_JOURNAL_TILE, journalWindowSettings)); + app.journalControllersList.add(journalController); + + if (IS_BROWSER) { + app.webAPI.openStageAsTab(journalStage, journalName.replace(" ", "_")); + } else { + journalStage.show(); + } + } +} 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) { 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 e05b9a7f6..000000000 --- a/application/src/main/java/dev/ikm/komet/app/WebApp.java +++ /dev/null @@ -1,1607 +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 { - - 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<>(); - - 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(); - private final StackPane rootPane = createRootPane(); - private Image appIcon; - private LandingPageController landingPageController; - private EvtBus 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"); - })); - } - - 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); - } - - @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(); - } - }, - () -> 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); - 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 = _ -> { - openImport(primaryStage); - }; - kViewEventBus.subscribe(IMPORT_TOPIC, Evt.class, importSubscriber); - } - - @Override - public void start(Stage stage) { - 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); - 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); - launchSelectDataSourcePage(stage); - } - - @Override - public void stop() { - LOG.info("Stopping application\n\n###############\n\n"); - - 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 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 = 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 { - 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 createExchangeMenu() { - Menu exchangeMenu = new Menu("Exchange"); - - MenuItem infoMenuItem = new MenuItem("Info"); - infoMenuItem.setOnAction(actionEvent -> infoAction()); - MenuItem pullMenuItem = new MenuItem("Pull"); - pullMenuItem.setOnAction(actionEvent -> executeGitTask(PULL)); - MenuItem pushMenuItem = new MenuItem("Sync"); - pushMenuItem.setOnAction(actionEvent -> executeGitTask(SYNC)); - - exchangeMenu.getItems().addAll(infoMenuItem, pullMenuItem, pushMenuItem); - return exchangeMenu; - } - - 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")); - BorderPane loginPane = loginNode.node(); - rootPane.getChildren().setAll(loginPane); - stage.setTitle("KOMET Login"); - - 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"); - - 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 -> { - 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); - }); - } - - private 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) { - createMenuOptions(landingPageBorderPane); - } - - landingPageController = landingPageLoader.getController(); - landingPageController.getWelcomeTitleLabel().setText("Welcome " + user.getName()); - landingPageController.setSelectedDatasetTitle(PrimitiveData.get().name()); - landingPageController.getGithubStatusHyperlink().setOnAction(_ -> 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); - - 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. - * 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) { - 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. - *

- * 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
-     * 
- */ - private 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 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) { - 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(); - } - } - - 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) { - 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(); - } - - private void openExport(Window 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(); - } - - /** - * 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. - *

- * 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 - 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(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; - } - - private void generateMsWindowsMenu(BorderPane kometRoot, Stage stage) { - MenuBar menuBar = new MenuBar(); - Menu fileMenu = new Menu("File"); - - MenuItem about = new MenuItem("About"); - about.setOnAction(_ -> 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() { - AboutDialog aboutDialog = new AboutDialog(); - aboutDialog.showAndWait(); - } - - private void quit() { - saveJournalWindowsToPreferences(); - PrimitiveData.stop(); - Preferences.stop(); - Platform.exit(); - stopServer(); - } - - /** - * Stops the JPro server by running the stop script. - */ - public static 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); - } - } - } - - 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 { - 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 - } -}