diff --git a/src/main/java/MarkNote.java b/src/main/java/MarkNote.java index 95fd3c6..8a5223f 100644 --- a/src/main/java/MarkNote.java +++ b/src/main/java/MarkNote.java @@ -29,6 +29,7 @@ import ui.ThemeTab; import ui.VisualLinkPanel; import ui.WelcomeTab; +import ui.WorkspaceRestoreOverlay; import ui.CommitDialog; import ui.AddRemoteDialog; import ui.UpdateDialog; @@ -69,6 +70,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.AnchorPane; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Screen; @@ -98,18 +100,19 @@ public class MarkNote extends Application { private BorderPane root; private HBox topBar; private HBox exitReadingModeBar; + private WorkspaceRestoreOverlay restoreOverlay; // Panels et SplitPanes pour la gestion de l'affichage private SplitPane editorSplit; private DockingManager dockingManager; private ConsolePanel consolePanel; - private CheckMenuItem showConsoleMenuItem; private boolean consoleDebugEnabled = false; private GitService gitService; private ProjectSessionService projectSessionService; private Debouncer previewDebouncer; + private Debouncer geometryDebouncer; // LLM Chat Panel private PromptPanel promptPanel; @@ -134,7 +137,8 @@ public class MarkNote extends Application { public static void main(String[] args) { // Prefer IPv6 when both IPv4/IPv6 exist for the same host. - // This avoids ConnectException on hosts where IPv4 resolves to a non-routable bridge address. + // This avoids ConnectException on hosts where IPv4 resolves to a non-routable + // bridge address. System.setProperty("java.net.preferIPv6Addresses", "true"); launch(args); } @@ -191,7 +195,8 @@ public void start(Stage stage) { projectExplorerPanel = new ProjectExplorerPanel(); projectExplorerPanel.setOnFileDoubleClick(this::openFileInTab); - // Synchronise l'arbre de l'explorateur et le diagramme réseau avec l'onglet actif + // Synchronise l'arbre de l'explorateur et le diagramme réseau avec l'onglet + // actif mainTabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> { if (newTab instanceof DocumentTab docTab) { File activeFile = docTab.getFile(); @@ -221,12 +226,14 @@ public void start(Stage stage) { // Project session service projectSessionService = new ProjectSessionService(); - // Sauvegarder la session à chaque ajout ou suppression d'onglet de document - mainTabPane.getTabs().addListener( - (javafx.collections.ListChangeListener) change -> { - saveProjectSession(); - } - ); + geometryDebouncer = new Debouncer(500); + // Sauvegarder la session à chaque ajout/suppression d'onglet et changement + // d'onglet actif + mainTabPane.getTabs().addListener((javafx.collections.ListChangeListener) change -> { + saveProjectSession(); + }); + mainTabPane.getSelectionModel().selectedItemProperty() + .addListener((obs, oldTab, newTab) -> saveProjectSession()); // Index service indexService = new IndexService(); @@ -253,8 +260,7 @@ public void start(Stage stage) { // Status bar (en bas de la fenêtre) statusBar = new StatusBar(); - statusBar.setPlantUmlIndicator( - config.isUseLocalPlantUml() && !config.getPlantUmlJarPath().isBlank()); + statusBar.setPlantUmlIndicator(config.isUseLocalPlantUml() && !config.getPlantUmlJarPath().isBlank()); // Callbacks de progression de l'indexation indexService.setOnProgress(progress -> statusBar.setIndexProgress(progress)); @@ -326,18 +332,14 @@ public void start(Stage stage) { return (sel instanceof DocumentTab dt) ? dt : null; }); // Fournit la liste de tous les DocumentTab ouverts pour la barre de contexte - promptPanel.setOpenTabsSupplier(() -> - mainTabPane.getTabs().stream() - .filter(t -> t instanceof DocumentTab) - .map(t -> (DocumentTab) t) - .toList() - ); + promptPanel.setOpenTabsSupplier(() -> mainTabPane.getTabs().stream().filter(t -> t instanceof DocumentTab) + .map(t -> (DocumentTab) t).toList()); // Rafraîchir la barre de documents à chaque ajout/suppression d'onglet - mainTabPane.getTabs().addListener( - (javafx.collections.ListChangeListener) change -> { - if (promptPanel != null) promptPanel.refreshDocumentContext(); - } - ); + mainTabPane.getTabs() + .addListener((javafx.collections.ListChangeListener) change -> { + if (promptPanel != null) + promptPanel.refreshDocumentContext(); + }); // Navigation vers un onglet depuis un lien de contexte dans le chat promptPanel.setOnOpenTab(tab -> mainTabPane.getSelectionModel().select(tab)); promptPanel.setOnDetach(() -> detachPanel(promptPanel)); @@ -386,7 +388,21 @@ public void start(Stage stage) { } }); - Scene scene = new Scene(root, 1200, 700); + // L'overlay recouvre toute la fenêtre via un AnchorPane (ancres 0 sur les 4 + // côtés) + restoreOverlay = new WorkspaceRestoreOverlay(messages); + AnchorPane sceneRoot = new AnchorPane(); + sceneRoot.getChildren().addAll(root, restoreOverlay); + AnchorPane.setTopAnchor(root, 0.0); + AnchorPane.setBottomAnchor(root, 0.0); + AnchorPane.setLeftAnchor(root, 0.0); + AnchorPane.setRightAnchor(root, 0.0); + AnchorPane.setTopAnchor(restoreOverlay, 0.0); + AnchorPane.setBottomAnchor(restoreOverlay, 0.0); + AnchorPane.setLeftAnchor(restoreOverlay, 0.0); + AnchorPane.setRightAnchor(restoreOverlay, 0.0); + + Scene scene = new Scene(sceneRoot, 1200, 700); applyTheme(scene); if (projectExplorerPanel.getProjectDirectory() == null) { stage.setTitle(messages.getString("app.title.editor")); @@ -394,6 +410,11 @@ public void start(Stage stage) { stage.setScene(scene); stage.setOnCloseRequest(e -> saveManagedPanelStates()); + // Restaurer la géométrie de fenêtre sauvegardée + restoreWindowGeometry(stage); + // Sauvegarder en temps réel pour survivre à un hard kill + installWindowGeometryListeners(stage); + // Icônes de fenêtre / barre des tâches (du plus petit au plus grand) String[] iconSizes = { "16", "32", "64", "128" }; for (String size : iconSizes) { @@ -405,38 +426,31 @@ public void start(Stage stage) { } } + // ShutdownHook pour SIGTERM / arrêt JVM hors FX (hard kill partiel) + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + saveManagedPanelStates(); + saveProjectSession(); + }, "marknote-shutdown-hook")); + // Afficher le splash screen ; la fenêtre principale et la logique de // démarrage s'exécutent une fois le splash fermé. if (config.isShowSplashScreen()) { SplashScreen splash = new SplashScreen(messages, config.getCurrentTheme()); splash.setOnClosed(() -> { - stage.show(); - // Rouvrir le dernier projet si l'option est activée - handleReopenLastProject(); - // Afficher l'onglet Welcome si l'option est activée - if (config.isShowWelcomePage()) { - showWelcomeTab(); - } - // Premier onglet (optionnel) - if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) { - addNewDocument(); - } - checkForUpdatesOnStartup(); + startWorking(stage); }); splash.show(); } else { - stage.show(); - handleReopenLastProject(); - if (config.isShowWelcomePage()) { - showWelcomeTab(); - } - if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) { - addNewDocument(); - } - checkForUpdatesOnStartup(); + startWorking(stage); } } + private void startWorking(Stage stage) { + stage.show(); + handleStartupRestore(); + checkForUpdatesOnStartup(); + } + /** * Crée la barre de menus. */ @@ -525,21 +539,22 @@ private MenuBar createMenuBar() { viewMenu.getItems().addAll(showProjectPanel, showPreviewPanel, new SeparatorMenuItem(), showTagCloud, showNetworkDiagram); - + // Ajouter l'option LLM si disponible if (showLLMPanelMenuItem != null) { viewMenu.getItems().add(showLLMPanelMenuItem); } - + MenuItem enterReadingModeItem = new MenuItem(messages.getString("menu.view.readingMode")); enterReadingModeItem.setAccelerator(KeyCombination.keyCombination("Ctrl+Shift+P")); enterReadingModeItem.setOnAction(e -> enterReadingMode()); - - viewMenu.getItems().addAll(new SeparatorMenuItem(), enterReadingModeItem, new SeparatorMenuItem(), showWelcomeItem); + + viewMenu.getItems().addAll(new SeparatorMenuItem(), enterReadingModeItem, new SeparatorMenuItem(), + showWelcomeItem); // Option Console (uniquement si --console-debug est actif) if (consoleDebugEnabled) { - showConsoleMenuItem = new CheckMenuItem(messages.getString("menu.view.console")); + CheckMenuItem showConsoleMenuItem = new CheckMenuItem(messages.getString("menu.view.console")); bindManagedPanelMenuItem(consolePanel, showConsoleMenuItem); viewMenu.getItems().add(viewMenu.getItems().size() - 1, new SeparatorMenuItem()); @@ -566,13 +581,15 @@ private MenuBar createMenuBar() { MenuItem searchItem = new MenuItem(messages.getString("menu.edit.search")); searchItem.setAccelerator(KeyCombination.keyCombination("Ctrl+F")); searchItem.setOnAction(e -> { - if (getActiveDocumentTab() != null) getActiveDocumentTab().openSearch(); + if (getActiveDocumentTab() != null) + getActiveDocumentTab().openSearch(); }); MenuItem replaceItem = new MenuItem(messages.getString("menu.edit.replace")); replaceItem.setAccelerator(KeyCombination.keyCombination("Ctrl+H")); replaceItem.setOnAction(e -> { - if (getActiveDocumentTab() != null) getActiveDocumentTab().openReplace(); + if (getActiveDocumentTab() != null) + getActiveDocumentTab().openReplace(); }); editMenu.getItems().addAll(searchItem, replaceItem); @@ -753,6 +770,24 @@ private void restoreManagedPanelStates() { detachPanel(panel, zone); } } + + // Restaurer les positions des dividers après la construction du layout + Platform.runLater(() -> { + if (editorSplit.getItems().size() > 1) { + double divider = config.getEditorSplitDivider(); + if (divider > 0.0 && divider < 1.0) { + editorSplit.setDividerPositions(divider); + } + } + double[] hDividers = config.getDockingHorizontalDividers(); + if (hDividers != null && hDividers.length > 0) { + dockingManager.setHorizontalDividers(hDividers); + } + double[] vDividers = config.getDockingVerticalDividers(); + if (vDividers != null && vDividers.length > 0) { + dockingManager.setVerticalDividers(vDividers); + } + }); } private void bindManagedPanelMenuItem(BasePanel panel, CheckMenuItem menuItem) { @@ -835,7 +870,8 @@ private void hideManagedPanel(BasePanel panel) { } private void enterReadingMode() { - if (readingModeActive) return; + if (readingModeActive) + return; readingModeActive = true; // Sauvegarder l'état du panneau de preview et la position du diviseur @@ -904,7 +940,8 @@ private void enterReadingMode() { projectExplorerPanel.setOnMinimize(() -> { if (!projectExplorerPanel.isPanelMinimized()) { - // Collapse: use the already-rendered header height (accurate since stage is shown). + // Collapse: use the already-rendered header height (accurate since stage is + // shown). // Chrome height = OS title bar = stage total − scene content area. double headerH = projectExplorerPanel.getHeader().getHeight(); double chromeH = readingModeFloatingStage.getHeight() @@ -940,7 +977,8 @@ private void exitReadingMode() { } private void exitReadingMode(boolean callSetFullScreen) { - if (!readingModeActive) return; + if (!readingModeActive) + return; readingModeActive = false; // Détacher l'explorateur de la scène flottante avant de le redocker @@ -975,15 +1013,18 @@ private void exitReadingMode(boolean callSetFullScreen) { editorSplit.getItems().remove(previewPanel); } - // Restaurer les panneaux qui étaient visibles (chaque appel peut déclencher rebuildLayout) + // Restaurer les panneaux qui étaient visibles (chaque appel peut déclencher + // rebuildLayout) for (Map.Entry entry : readingModePanelVisibility.entrySet()) { if (entry.getValue()) { showManagedPanel(entry.getKey()); } } - // Restaurer les positions des diviseurs après que tous les rebuildLayout ont été faits. - // Platform.runLater garantit que les positions sont appliquées après le passage de layout. + // Restaurer les positions des diviseurs après que tous les rebuildLayout ont + // été faits. + // Platform.runLater garantit que les positions sont appliquées après le passage + // de layout. final double savedEditorDivider = readingModeEditorSplitDivider; final double[] savedHDividers = readingModeDockingHDividers; final double[] savedVDividers = readingModeDockingVDividers; @@ -1004,12 +1045,20 @@ private void saveManagedPanelStates() { DockZone zone = getStoredOrDefaultZone(panel); config.setPanelState(panel.getPanelStateId(), new AppConfig.PanelState(visible, docked, zone.name())); } + + // Sauvegarder les positions des dividers + if (editorSplit.getItems().size() > 1 && !editorSplit.getDividers().isEmpty()) { + config.setEditorSplitDivider(editorSplit.getDividers().get(0).getPosition()); + } + config.setDockingHorizontalDividers(dockingManager.getHorizontalDividers()); + config.setDockingVerticalDividers(dockingManager.getVerticalDividers()); + config.save(); } /** - * Met en valeur le document actif dans le VisualLinkPanel. - * À appeler à chaque changement d'onglet actif ou quand le panel devient visible. + * Met en valeur le document actif dans le VisualLinkPanel. À appeler à chaque + * changement d'onglet actif ou quand le panel devient visible. */ private void syncVisualLinkToActiveTab() { var selected = mainTabPane.getSelectionModel().getSelectedItem(); @@ -1085,10 +1134,10 @@ private void setupDocumentTab(DocumentTab tab) { } }); - // Synchroniser le défilement éditeur → preview (pas en reading mode : éditeur hors-scène) + // Synchroniser le défilement éditeur → preview (pas en reading mode : éditeur + // hors-scène) tab.setOnScrollFractionChanged(fraction -> { - if (!readingModeActive - && mainTabPane.getSelectionModel().getSelectedItem() == tab + if (!readingModeActive && mainTabPane.getSelectionModel().getSelectedItem() == tab && editorSplit.getItems().contains(previewPanel)) { previewPanel.scrollToFraction(fraction); } @@ -1212,9 +1261,8 @@ private File createNewProjectDirectory() { File newProjectDir = new File(parentDir, projectName); if (newProjectDir.exists() || !newProjectDir.mkdirs()) { - showError(messages.getString("error.projectCreate.title"), - MessageFormat.format(messages.getString("error.projectCreate.message"), - newProjectDir.getAbsolutePath())); + showError(messages.getString("error.projectCreate.title"), MessageFormat + .format(messages.getString("error.projectCreate.message"), newProjectDir.getAbsolutePath())); return null; } @@ -1225,24 +1273,34 @@ private File createNewProjectDirectory() { * Ouvre un répertoire de projet et met à jour l'UI/l'historique. */ private void openProjectDirectory(File dir) { - // Configurer le service git avec les credentials de la config + setupProjectDirectory(dir); + ProjectSessionService.SessionData session = projectSessionService.loadSession(dir); + for (File f : session.openFiles()) { + openFileInTab(f); + } + if (session.activeFile() != null) { + final File target = session.activeFile(); + mainTabPane.getTabs().stream().filter(t -> t instanceof DocumentTab dt && target.equals(dt.getFile())) + .findFirst().ifPresent(t -> mainTabPane.getSelectionModel().select(t)); + } + } + + /** + * Configure le projet (git, explorateur, titre, index) sans ouvrir les fichiers + * de session. Appelé à la fois par {@link #openProjectDirectory} et par la + * restauration avec overlay. + */ + private void setupProjectDirectory(File dir) { gitService.setSshKeyPath(config.getGitSshKeyPath()); gitService.setGitToken(config.getGitToken()); gitService.setGitUsername(config.getGitUsername()); gitService.setProject(dir); - projectExplorerPanel.setProjectDirectory(dir); previewPanel.setBaseDirectory(dir); primaryStage.setTitle(messages.getString("app.title") + " - " + dir.getName()); config.addRecentDir(dir); refreshRecentMenu(); loadOrBuildIndex(dir); - - // Rouvrir les documents de la session précédente - List sessionFiles = projectSessionService.loadSession(dir); - for (File f : sessionFiles) { - openFileInTab(f); - } } /** @@ -1399,6 +1457,146 @@ private void handleReopenLastProject() { } } + /** + * Point d'entrée unique pour la logique de démarrage (après splash éventuel). + * Si restoreWorkspaceOnStart est actif, le dernier workspace est restauré + * automatiquement avec un overlay de progression. Sinon, le comportement + * historique (dialogue de confirmation) est conservé. + */ + private void handleStartupRestore() { + if (config.isRestoreWorkspaceOnStart() && !config.getRecentDirs().isEmpty()) { + File lastDir = new File(config.getRecentDirs().getFirst()); + if (lastDir.exists() && lastDir.isDirectory()) { + restoreOverlay.show(); + setupProjectDirectory(lastDir); + ProjectSessionService.SessionData session = projectSessionService.loadSession(lastDir); + openSessionFilesSequentially(session.openFiles(), 0, session.activeFile()); + return; + } + } + // Comportement historique : dialogue de confirmation ou onglet de bienvenue + handleReopenLastProject(); + if (config.isShowWelcomePage()) { + showWelcomeTab(); + } + if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) { + addNewDocument(); + } + } + + /** + * Ouvre les fichiers de session un par un en enchaînant des + * {@link Platform#runLater} pour que le FX thread puisse rafraîchir l'overlay + * entre chaque fichier. + */ + private void openSessionFilesSequentially(List files, int index, File activeFile) { + if (index >= files.size()) { + // Tous les fichiers ouverts : restaurer l'onglet actif puis masquer l'overlay + if (activeFile != null) { + final File target = activeFile; + mainTabPane.getTabs().stream().filter(t -> t instanceof DocumentTab dt && target.equals(dt.getFile())) + .findFirst().ifPresent(t -> mainTabPane.getSelectionModel().select(t)); + } + restoreOverlay.hide(); + return; + } + File f = files.get(index); + restoreOverlay.setProgress(f.getName(), index + 1, files.size()); + openFileInTab(f); + Platform.runLater(() -> openSessionFilesSequentially(files, index + 1, activeFile)); + } + + /** + * Applique la géométrie de fenêtre sauvegardée (position, taille, mode plein + * écran / maximisé). Doit être appelé avant {@code stage.show()} pour éviter un + * flash. + */ + private void restoreWindowGeometry(Stage stage) { + if (config.getWindowWidth() > 100) { + stage.setWidth(config.getWindowWidth()); + } + if (config.getWindowHeight() > 100) { + stage.setHeight(config.getWindowHeight()); + } + if (config.getWindowX() >= 0) { + stage.setX(config.getWindowX()); + } + if (config.getWindowY() >= 0) { + stage.setY(config.getWindowY()); + } + if (config.isWindowMaximized()) { + stage.setMaximized(true); + } + // Le fullscreen doit être appliqué après que la fenêtre soit affichée (macOS + // green button) + if (config.isWindowFullscreen()) { + stage.showingProperty().addListener(new javafx.beans.value.ChangeListener<>() { + @Override + public void changed(javafx.beans.value.ObservableValue obs, Boolean wasShowing, + Boolean isShowing) { + if (isShowing) { + stage.showingProperty().removeListener(this); + Platform.runLater(() -> { + stage.setFullScreenExitHint(""); + stage.setFullScreen(true); + }); + } + } + }); + } + } + + /** + * Installe des listeners sur les propriétés de géométrie de fenêtre pour + * sauvegarder l'état en temps réel. Cela garantit la survie de l'état même + * après un hard kill (SIGKILL). La position et la taille sont sauvegardées avec + * un debounce de 500 ms pour éviter d'écrire la config à chaque pixel lors d'un + * redimensionnement. + */ + private void installWindowGeometryListeners(Stage stage) { + // Un seul Runnable partagé qui sauvegarde les 4 dimensions d'un coup. + // Chaque listener déclenche le même debounce, évitant qu'une propriété + // n'écrase les autres quand plusieurs changent simultanément (ex: resize). + Runnable saveGeometry = () -> Platform.runLater(() -> { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) { + config.setWindowX(stage.getX()); + config.setWindowY(stage.getY()); + config.setWindowWidth(stage.getWidth()); + config.setWindowHeight(stage.getHeight()); + config.save(); + } + }); + + stage.xProperty().addListener((obs, o, n) -> { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) + geometryDebouncer.debounce(saveGeometry); + }); + stage.yProperty().addListener((obs, o, n) -> { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) + geometryDebouncer.debounce(saveGeometry); + }); + stage.widthProperty().addListener((obs, o, n) -> { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) + geometryDebouncer.debounce(saveGeometry); + }); + stage.heightProperty().addListener((obs, o, n) -> { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) + geometryDebouncer.debounce(saveGeometry); + }); + stage.maximizedProperty().addListener((obs, o, maximized) -> { + if (!readingModeActive) { + config.setWindowMaximized(maximized); + config.save(); + } + }); + stage.fullScreenProperty().addListener((obs, o, fullscreen) -> { + if (!readingModeActive) { + config.setWindowFullscreen(fullscreen); + config.save(); + } + }); + } + /** * Affiche le dialogue d'options. */ @@ -1416,8 +1614,7 @@ private void showOptionsDialog() { // Refresh preview in case PlantUML settings changed previewPanel.refresh(); // Update PlantUML status bar indicator - statusBar.setPlantUmlIndicator( - config.isUseLocalPlantUml() && !config.getPlantUmlJarPath().isBlank()); + statusBar.setPlantUmlIndicator(config.isUseLocalPlantUml() && !config.getPlantUmlJarPath().isBlank()); // Update git credentials gitService.setSshKeyPath(config.getGitSshKeyPath()); gitService.setGitToken(config.getGitToken()); @@ -1427,11 +1624,11 @@ private void showOptionsDialog() { } /** - * Affiche le résultat d'une opération git (pull/push) dans une boîte de dialogue. + * Affiche le résultat d'une opération git (pull/push) dans une boîte de + * dialogue. */ private void showGitOperationResult(String result) { - Alert alert = new Alert( - result.startsWith("Error:") ? Alert.AlertType.ERROR : Alert.AlertType.INFORMATION); + Alert alert = new Alert(result.startsWith("Error:") ? Alert.AlertType.ERROR : Alert.AlertType.INFORMATION); alert.initOwner(primaryStage); alert.setTitle(messages.getString("git.operation.result.title")); alert.setHeaderText(null); @@ -1445,20 +1642,21 @@ private void showGitOperationResult(String result) { /** Ouvre la boîte de dialogue de commit git. */ private void handleGitCommit() { - new CommitDialog(primaryStage, gitService).showAndWait() - .ifPresent(msg -> gitService.commitAsync(msg)); + new CommitDialog(primaryStage, gitService).showAndWait().ifPresent(msg -> gitService.commitAsync(msg)); } /** Ouvre la boîte de dialogue d'ajout d'un remote git. */ private void handleGitAddRemote() { boolean saved = new AddRemoteDialog(primaryStage, gitService, config).showAndWait(); - if (saved) projectExplorerPanel.refresh(); + if (saved) + projectExplorerPanel.refresh(); } /** Initialise un dépôt git dans le répertoire de projet courant. */ private void handleGitInit() { File projectDir = projectExplorerPanel.getProjectDirectory(); - if (projectDir == null) return; + if (projectDir == null) + return; try { gitService.init(projectDir); // Vérifier que l'identité est configurée @@ -1490,9 +1688,8 @@ private void handleGitInit() { confirm.showAndWait().ifPresent(bt -> { if (bt == javafx.scene.control.ButtonType.OK) { gitService.addAllAsync(); - javafx.application.Platform.runLater(() -> - new CommitDialog(primaryStage, gitService).showAndWait() - .ifPresent(msg -> gitService.commitAsync(msg))); + javafx.application.Platform.runLater(() -> new CommitDialog(primaryStage, gitService).showAndWait() + .ifPresent(msg -> gitService.commitAsync(msg))); } }); projectExplorerPanel.refresh(); @@ -1534,27 +1731,28 @@ private void showAboutDialog() { } private void checkForUpdatesOnStartup() { - if (!config.isAutoCheckUpdate()) return; + if (!config.isAutoCheckUpdate()) + return; new Thread(() -> { - UpdateChecker.VersionInfo info = - UpdateChecker.checkForUpdate(messages.getString("app.version")); - if (info == null) return; - if (info.tagName().equals(config.getSkipVersion())) return; + UpdateChecker.VersionInfo info = UpdateChecker.checkForUpdate(messages.getString("app.version")); + if (info == null) + return; + if (info.tagName().equals(config.getSkipVersion())) + return; Platform.runLater(() -> showUpdateDialog(info)); }, "update-check").start(); } private void checkForUpdatesManual() { new Thread(() -> { - UpdateChecker.VersionInfo info = - UpdateChecker.checkForUpdate(messages.getString("app.version")); + UpdateChecker.VersionInfo info = UpdateChecker.checkForUpdate(messages.getString("app.version")); Platform.runLater(() -> { if (info == null) { Alert a = new Alert(Alert.AlertType.INFORMATION); a.initOwner(primaryStage); a.setTitle(messages.getString("update.uptodate.title")); - a.setContentText(messages.getString("update.uptodate.content") - .replace("{0}", messages.getString("app.version"))); + a.setContentText(messages.getString("update.uptodate.content").replace("{0}", + messages.getString("app.version"))); a.showAndWait(); } else { showUpdateDialog(info); @@ -1564,8 +1762,7 @@ private void checkForUpdatesManual() { } private void showUpdateDialog(UpdateChecker.VersionInfo info) { - UpdateDialog dlg = new UpdateDialog(messages, primaryStage, info, - messages.getString("app.version")); + UpdateDialog dlg = new UpdateDialog(messages, primaryStage, info, messages.getString("app.version")); UpdateDialog.UpdateResult result = dlg.showAndGet(); if (result == UpdateDialog.UpdateResult.SKIP) { config.setSkipVersion(info.tagName()); @@ -1578,16 +1775,12 @@ private void showUpdateDialog(UpdateChecker.VersionInfo info) { private void downloadAndInstall(UpdateChecker.VersionInfo info) { new Thread(() -> { try { - String suffix = info.assetName() - .substring(info.assetName().lastIndexOf('.')); + String suffix = info.assetName().substring(info.assetName().lastIndexOf('.')); Path dest = Files.createTempFile("marknote-update-", suffix); - HttpClient client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build(); + HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build(); client.send( HttpRequest.newBuilder(URI.create(info.downloadUrl())) - .header("Accept", "application/octet-stream") - .build(), + .header("Accept", "application/octet-stream").build(), HttpResponse.BodyHandlers.ofFile(dest)); java.awt.Desktop.getDesktop().open(dest.toFile()); } catch (Exception e) { @@ -1667,13 +1860,14 @@ private void showError(String title, String message) { */ private void saveProjectSession() { File projectDir = projectExplorerPanel.getProjectDirectory(); - if (projectDir == null) return; + if (projectDir == null) + return; List openFiles = mainTabPane.getTabs().stream() - .filter(t -> t instanceof DocumentTab dt && dt.getFile() != null) - .map(t -> ((DocumentTab) t).getFile()) - .filter(f -> f.toPath().startsWith(projectDir.toPath())) - .toList(); - projectSessionService.saveSession(projectDir, openFiles); + .filter(t -> t instanceof DocumentTab dt && dt.getFile() != null).map(t -> ((DocumentTab) t).getFile()) + .filter(f -> f.toPath().startsWith(projectDir.toPath())).toList(); + DocumentTab activeDocTab = getActiveDocumentTab(); + File activeFile = (activeDocTab != null && activeDocTab.getFile() != null) ? activeDocTab.getFile() : null; + projectSessionService.saveSession(projectDir, openFiles, activeFile); } @Override @@ -1683,6 +1877,9 @@ public void stop() { if (previewDebouncer != null) { previewDebouncer.shutdown(); } + if (geometryDebouncer != null) { + geometryDebouncer.shutdown(); + } } // ── Status bar helpers ────────────────────────────────────────── diff --git a/src/main/java/config/AppConfig.java b/src/main/java/config/AppConfig.java index 4c7a268..e9593da 100644 --- a/src/main/java/config/AppConfig.java +++ b/src/main/java/config/AppConfig.java @@ -33,16 +33,32 @@ public class AppConfig { private boolean reattachDiagramOnTabClose = true; private final Map panelStates = new HashMap<>(); + // Panel divider positions + private double editorSplitDivider = 0.5; + private double[] dockingHorizontalDividers = new double[0]; + private double[] dockingVerticalDividers = new double[0]; + // Git credentials (V1: SSH passphrase-less + HTTPS token) private String gitSshKeyPath = ""; - private String gitToken = ""; - private String gitUsername = "token"; + private String gitToken = ""; + private String gitUsername = "token"; private String gitToolbarMode = "standard"; + // Window geometry — persisted so workspace survives hard kill + private double windowX = -1; + private double windowY = -1; + private double windowWidth = 1200; + private double windowHeight = 700; + private boolean windowMaximized = false; + private boolean windowFullscreen = false; + + // Workspace restore + private boolean restoreWorkspaceOnStart = true; private boolean autoCheckUpdate = true; private String skipVersion = null; - public record PanelState(boolean visible, boolean docked, String zone) {} + public record PanelState(boolean visible, boolean docked, String zone) { + } /** * Charge la configuration depuis le fichier. @@ -84,13 +100,15 @@ public void load() { } else if (line.startsWith("language=")) { language = line.substring("language=".length()).trim(); } else if (line.startsWith("frontMatterExpandedByDefault=")) { - frontMatterExpandedByDefault = Boolean.parseBoolean(line.substring("frontMatterExpandedByDefault=".length()).trim()); + frontMatterExpandedByDefault = Boolean + .parseBoolean(line.substring("frontMatterExpandedByDefault=".length()).trim()); } else if (line.startsWith("useLocalPlantUml=")) { useLocalPlantUml = Boolean.parseBoolean(line.substring("useLocalPlantUml=".length()).trim()); } else if (line.startsWith("plantUmlJarPath=")) { plantUmlJarPath = line.substring("plantUmlJarPath=".length()).trim(); } else if (line.startsWith("reattachDiagramOnTabClose=")) { - reattachDiagramOnTabClose = Boolean.parseBoolean(line.substring("reattachDiagramOnTabClose=".length()).trim()); + reattachDiagramOnTabClose = Boolean + .parseBoolean(line.substring("reattachDiagramOnTabClose=".length()).trim()); } else if (line.startsWith("gitSshKeyPath=")) { gitSshKeyPath = line.substring("gitSshKeyPath=".length()).trim(); } else if (line.startsWith("gitToken=")) { @@ -99,19 +117,76 @@ public void load() { gitUsername = line.substring("gitUsername=".length()).trim(); } else if (line.startsWith("gitToolbarMode=")) { gitToolbarMode = line.substring("gitToolbarMode=".length()).trim(); + + } else if (line.startsWith("windowX=")) { + try { + windowX = Double.parseDouble(line.substring("windowX=".length()).trim()); + } catch (NumberFormatException ignored) { + } + } else if (line.startsWith("windowY=")) { + try { + windowY = Double.parseDouble(line.substring("windowY=".length()).trim()); + } catch (NumberFormatException ignored) { + } + } else if (line.startsWith("windowWidth=")) { + try { + windowWidth = Double.parseDouble(line.substring("windowWidth=".length()).trim()); + } catch (NumberFormatException ignored) { + } + } else if (line.startsWith("windowHeight=")) { + try { + windowHeight = Double.parseDouble(line.substring("windowHeight=".length()).trim()); + } catch (NumberFormatException ignored) { + } + } else if (line.startsWith("windowMaximized=")) { + windowMaximized = Boolean.parseBoolean(line.substring("windowMaximized=".length()).trim()); + } else if (line.startsWith("windowFullscreen=")) { + windowFullscreen = Boolean.parseBoolean(line.substring("windowFullscreen=".length()).trim()); + } else if (line.startsWith("restoreWorkspaceOnStart=")) { + restoreWorkspaceOnStart = Boolean + .parseBoolean(line.substring("restoreWorkspaceOnStart=".length()).trim()); + } else if (line.startsWith("autoCheckUpdate=")) { autoCheckUpdate = Boolean.parseBoolean(line.substring("autoCheckUpdate=".length()).trim()); } else if (line.startsWith("skipVersion=")) { String v = line.substring("skipVersion=".length()).trim(); skipVersion = v.isEmpty() ? null : v; + } else if (line.startsWith("panelState=")) { String raw = line.substring("panelState=".length()).trim(); String[] parts = raw.split("\\|", -1); if (parts.length >= 4 && !parts[0].isBlank()) { - panelStates.put(parts[0], new PanelState( - Boolean.parseBoolean(parts[1]), - Boolean.parseBoolean(parts[2]), - parts[3].trim())); + panelStates.put(parts[0], new PanelState(Boolean.parseBoolean(parts[1]), + Boolean.parseBoolean(parts[2]), parts[3].trim())); + } + } else if (line.startsWith("editorSplitDivider=")) { + try { + editorSplitDivider = Double.parseDouble(line.substring("editorSplitDivider=".length()).trim()); + } catch (NumberFormatException ignored) { + } + } else if (line.startsWith("dockingHDividers=")) { + String raw = line.substring("dockingHDividers=".length()).trim(); + if (!raw.isEmpty()) { + String[] parts = raw.split(","); + dockingHorizontalDividers = new double[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + dockingHorizontalDividers[i] = Double.parseDouble(parts[i].trim()); + } catch (NumberFormatException ignored) { + } + } + } + } else if (line.startsWith("dockingVDividers=")) { + String raw = line.substring("dockingVDividers=".length()).trim(); + if (!raw.isEmpty()) { + String[] parts = raw.split(","); + dockingVerticalDividers = new double[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + dockingVerticalDividers[i] = Double.parseDouble(parts[i].trim()); + } catch (NumberFormatException ignored) { + } + } } } } @@ -129,7 +204,7 @@ public void save() { if (!configDir.exists()) { configDir.mkdirs(); } - + List lines = new ArrayList<>(); lines.add("maxRecentItems=" + maxRecentItems); lines.add("openDocOnStart=" + openDocOnStart); @@ -146,11 +221,36 @@ public void save() { lines.add("gitToken=" + gitToken); lines.add("gitUsername=" + gitUsername); lines.add("gitToolbarMode=" + gitToolbarMode); + lines.add("windowX=" + windowX); + lines.add("windowY=" + windowY); + lines.add("windowWidth=" + windowWidth); + lines.add("windowHeight=" + windowHeight); + lines.add("windowMaximized=" + windowMaximized); + lines.add("windowFullscreen=" + windowFullscreen); + lines.add("restoreWorkspaceOnStart=" + restoreWorkspaceOnStart); lines.add("autoCheckUpdate=" + autoCheckUpdate); lines.add("skipVersion=" + (skipVersion != null ? skipVersion : "")); + lines.add("editorSplitDivider=" + editorSplitDivider); + if (dockingHorizontalDividers.length > 0) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < dockingHorizontalDividers.length; i++) { + if (i > 0) sb.append(","); + sb.append(dockingHorizontalDividers[i]); + } + lines.add("dockingHDividers=" + sb.toString()); + } + if (dockingVerticalDividers.length > 0) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < dockingVerticalDividers.length; i++) { + if (i > 0) sb.append(","); + sb.append(dockingVerticalDividers[i]); + } + lines.add("dockingVDividers=" + sb.toString()); + } for (Map.Entry entry : panelStates.entrySet()) { PanelState state = entry.getValue(); - lines.add("panelState=" + entry.getKey() + "|" + state.visible() + "|" + state.docked() + "|" + state.zone()); + lines.add("panelState=" + entry.getKey() + "|" + state.visible() + "|" + state.docked() + "|" + + state.zone()); } for (String f : recentFiles) { lines.add("recentFile=" + f); @@ -309,23 +409,53 @@ public void setReattachDiagramOnTabClose(boolean reattachDiagramOnTabClose) { this.reattachDiagramOnTabClose = reattachDiagramOnTabClose; } - public String getGitSshKeyPath() { return gitSshKeyPath; } - public void setGitSshKeyPath(String path) { this.gitSshKeyPath = path != null ? path : ""; } + public String getGitSshKeyPath() { + return gitSshKeyPath; + } - public String getGitToken() { return gitToken; } - public void setGitToken(String token) { this.gitToken = token != null ? token : ""; } + public void setGitSshKeyPath(String path) { + this.gitSshKeyPath = path != null ? path : ""; + } - public String getGitUsername() { return gitUsername; } - public void setGitUsername(String username) { this.gitUsername = username != null ? username : "token"; } + public String getGitToken() { + return gitToken; + } + + public void setGitToken(String token) { + this.gitToken = token != null ? token : ""; + } + + public String getGitUsername() { + return gitUsername; + } + + public void setGitUsername(String username) { + this.gitUsername = username != null ? username : "token"; + } + + public String getGitToolbarMode() { + return gitToolbarMode; + } + + public void setGitToolbarMode(String mode) { + this.gitToolbarMode = (mode != null && !mode.isBlank()) ? mode : "standard"; + } + + public boolean isAutoCheckUpdate() { + return autoCheckUpdate; + } - public String getGitToolbarMode() { return gitToolbarMode; } - public void setGitToolbarMode(String mode) { this.gitToolbarMode = (mode != null && !mode.isBlank()) ? mode : "standard"; } + public void setAutoCheckUpdate(boolean autoCheckUpdate) { + this.autoCheckUpdate = autoCheckUpdate; + } - public boolean isAutoCheckUpdate() { return autoCheckUpdate; } - public void setAutoCheckUpdate(boolean autoCheckUpdate) { this.autoCheckUpdate = autoCheckUpdate; } + public String getSkipVersion() { + return skipVersion; + } - public String getSkipVersion() { return skipVersion; } - public void setSkipVersion(String version) { this.skipVersion = version; } + public void setSkipVersion(String version) { + this.skipVersion = version; + } /** * Supprime un fichier de la liste des récents. @@ -365,4 +495,84 @@ public boolean hasPanelState(String panelId) { public boolean hasAnyPanelStates() { return !panelStates.isEmpty(); } + + public double getWindowX() { + return windowX; + } + + public void setWindowX(double v) { + this.windowX = v; + } + + public double getWindowY() { + return windowY; + } + + public void setWindowY(double v) { + this.windowY = v; + } + + public double getWindowWidth() { + return windowWidth; + } + + public void setWindowWidth(double v) { + this.windowWidth = v; + } + + public double getWindowHeight() { + return windowHeight; + } + + public void setWindowHeight(double v) { + this.windowHeight = v; + } + + public boolean isWindowMaximized() { + return windowMaximized; + } + + public void setWindowMaximized(boolean v) { + this.windowMaximized = v; + } + + public boolean isWindowFullscreen() { + return windowFullscreen; + } + + public void setWindowFullscreen(boolean v) { + this.windowFullscreen = v; + } + + public boolean isRestoreWorkspaceOnStart() { + return restoreWorkspaceOnStart; + } + + public void setRestoreWorkspaceOnStart(boolean v) { + this.restoreWorkspaceOnStart = v; + } + + public double getEditorSplitDivider() { + return editorSplitDivider; + } + + public void setEditorSplitDivider(double editorSplitDivider) { + this.editorSplitDivider = editorSplitDivider; + } + + public double[] getDockingHorizontalDividers() { + return dockingHorizontalDividers; + } + + public void setDockingHorizontalDividers(double[] dockingHorizontalDividers) { + this.dockingHorizontalDividers = dockingHorizontalDividers != null ? dockingHorizontalDividers : new double[0]; + } + + public double[] getDockingVerticalDividers() { + return dockingVerticalDividers; + } + + public void setDockingVerticalDividers(double[] dockingVerticalDividers) { + this.dockingVerticalDividers = dockingVerticalDividers != null ? dockingVerticalDividers : new double[0]; + } } diff --git a/src/main/java/ui/WorkspaceRestoreOverlay.java b/src/main/java/ui/WorkspaceRestoreOverlay.java new file mode 100644 index 0000000..9fed583 --- /dev/null +++ b/src/main/java/ui/WorkspaceRestoreOverlay.java @@ -0,0 +1,123 @@ +package ui; + +import java.text.MessageFormat; +import java.util.ResourceBundle; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.effect.DropShadow; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; + +/** + * Overlay plein écran affiché pendant la restauration du workspace au démarrage. + * Doit être ajouté en dernier enfant d'un {@link StackPane} pour couvrir toute la fenêtre. + */ +public class WorkspaceRestoreOverlay extends StackPane { + + private final Label statusLabel; + private final ProgressBar progressBar; + private final ResourceBundle messages; + + public WorkspaceRestoreOverlay(ResourceBundle messages) { + this.messages = messages; + + setAlignment(Pos.CENTER); + setPickOnBounds(true); // bloque les clics pendant le chargement + + // Rectangle de fond : lié à notre propre taille, insensible au CSS du thème + Rectangle backdrop = new Rectangle(); + backdrop.setFill(Color.rgb(0, 0, 0, 0.52)); + backdrop.widthProperty().bind(widthProperty()); + backdrop.heightProperty().bind(heightProperty()); + + // ── Carte centrale ──────────────────────────────────────────── + VBox card = new VBox(14); + card.setAlignment(Pos.CENTER); + card.setPadding(new Insets(40, 64, 40, 64)); + card.setMaxWidth(480); + card.setMaxHeight(480); + card.setStyle( + "-fx-background-color: rgba(255,255,255,0.96);" + + "-fx-background-radius: 14;" + + "-fx-border-radius: 14;" + ); + card.setEffect(new DropShadow(32, 0, 8, Color.rgb(0, 0, 0, 0.35))); + + // Logo + ImageView logo = loadLogo(); + + // Titre + Label titleLabel = new Label(messages.getString("restore.overlay.title")); + titleLabel.setStyle("-fx-font-size: 15px; -fx-font-weight: bold; -fx-text-fill: #2c3440;"); + titleLabel.setWrapText(true); + titleLabel.setMaxWidth(580); + titleLabel.setAlignment(Pos.CENTER); + + // Barre de progression + progressBar = new ProgressBar(ProgressBar.INDETERMINATE_PROGRESS); + progressBar.setPrefWidth(520); + progressBar.setPrefHeight(8); + progressBar.setStyle("-fx-accent: #3584e4;"); + + // Fichier en cours + statusLabel = new Label(" "); + statusLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #6b7a8f;"); + statusLabel.setWrapText(true); + statusLabel.setMaxWidth(580); + statusLabel.setAlignment(Pos.CENTER); + + if (logo != null) { + card.getChildren().addAll(logo, titleLabel, progressBar, statusLabel); + } else { + card.getChildren().addAll(titleLabel, progressBar, statusLabel); + } + getChildren().addAll(backdrop, card); + setVisible(false); + } + + /** Affiche l'overlay avec la barre indéterminée. */ + public void show() { + progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + statusLabel.setText(" "); + setVisible(true); + toFront(); + } + + /** + * Met à jour la progression et le nom du fichier en cours d'ouverture. + * + * @param filename nom du fichier + * @param current index 1-based du fichier en cours + * @param total nombre total de fichiers + */ + public void setProgress(String filename, int current, int total) { + progressBar.setProgress(total > 0 ? (double) current / total : ProgressBar.INDETERMINATE_PROGRESS); + statusLabel.setText(MessageFormat.format(messages.getString("restore.overlay.file"), filename)); + } + + /** Masque l'overlay. */ + public void hide() { + setVisible(false); + } + + private ImageView loadLogo() { + try (var is = getClass().getResourceAsStream("/images/icons/marknote-64.png")) { + if (is != null) { + ImageView iv = new ImageView(new Image(is)); + iv.setFitWidth(48); + iv.setFitHeight(48); + iv.setPreserveRatio(true); + iv.setSmooth(true); + return iv; + } + } catch (Exception ignored) {} + return null; + } +} diff --git a/src/main/java/utils/ProjectSessionService.java b/src/main/java/utils/ProjectSessionService.java index cb1ebd4..7da1963 100644 --- a/src/main/java/utils/ProjectSessionService.java +++ b/src/main/java/utils/ProjectSessionService.java @@ -18,81 +18,96 @@ public class ProjectSessionService { private static final String SESSION_FILE = ".marknote"; private static final String SECTION_OPEN_FILES = "[open_files]"; + private static final String SECTION_ACTIVE_FILE = "[active_file]"; private final LogService log = LogService.getInstance(); private static final String LOG_SOURCE = "ProjectSessionService"; + /** État de session d'un projet : fichiers ouverts + onglet actif. */ + public record SessionData(List openFiles, File activeFile) {} + /** - * Sauvegarde la liste des fichiers ouverts dans {@code /.marknote}. + * Sauvegarde la session du projet (fichiers ouverts + onglet actif) dans + * {@code /.marknote}. * * @param projectDir répertoire racine du projet - * @param openFiles chemins absolus des fichiers actuellement ouverts + * @param openFiles fichiers actuellement ouverts + * @param activeFile fichier actif (onglet sélectionné), peut être null */ - public void saveSession(File projectDir, List openFiles) { + public void saveSession(File projectDir, List openFiles, File activeFile) { if (projectDir == null) return; Path sessionPath = projectDir.toPath().resolve(SESSION_FILE); List lines = new ArrayList<>(); lines.add(SECTION_OPEN_FILES); for (File f : openFiles) { - // Stocker le chemin relatif au répertoire projet pour la portabilité try { String relative = projectDir.toPath().relativize(f.toPath()).toString(); lines.add(relative); } catch (IllegalArgumentException e) { - // Fichier hors du projet : chemin absolu en fallback lines.add(f.getAbsolutePath()); } } + if (activeFile != null) { + lines.add(SECTION_ACTIVE_FILE); + try { + lines.add(projectDir.toPath().relativize(activeFile.toPath()).toString()); + } catch (IllegalArgumentException e) { + lines.add(activeFile.getAbsolutePath()); + } + } try { Files.writeString(sessionPath, String.join("\n", lines) + "\n"); - log.info(LOG_SOURCE, "Session saved: " + openFiles.size() + " file(s)"); + log.info(LOG_SOURCE, "Session saved: " + openFiles.size() + " file(s), active=" + + (activeFile != null ? activeFile.getName() : "none")); } catch (IOException e) { log.error(LOG_SOURCE, "Failed to save session: " + e.getMessage()); } } /** - * Charge la liste des fichiers à rouvrir depuis {@code /.marknote}. - * Seuls les fichiers qui existent réellement sur le disque sont retournés. + * Charge la session depuis {@code /.marknote}. * * @param projectDir répertoire racine du projet - * @return liste des fichiers à rouvrir (jamais null) + * @return données de session (jamais null) */ - public List loadSession(File projectDir) { - List result = new ArrayList<>(); - if (projectDir == null) return result; + public SessionData loadSession(File projectDir) { + List openFiles = new ArrayList<>(); + File activeFile = null; + if (projectDir == null) return new SessionData(openFiles, null); Path sessionPath = projectDir.toPath().resolve(SESSION_FILE); - if (!Files.exists(sessionPath)) return result; + if (!Files.exists(sessionPath)) return new SessionData(openFiles, null); try { List lines = Files.readAllLines(sessionPath); - boolean inSection = false; + String currentSection = null; for (String line : lines) { String trimmed = line.strip(); if (trimmed.isEmpty()) continue; - if (SECTION_OPEN_FILES.equals(trimmed)) { - inSection = true; - continue; - } if (trimmed.startsWith("[")) { - inSection = false; + currentSection = trimmed; continue; } - if (inSection) { + if (SECTION_OPEN_FILES.equals(currentSection)) { + File f = resolveFile(projectDir, trimmed); + if (f != null && f.exists() && f.isFile()) { + openFiles.add(f); + } + } else if (SECTION_ACTIVE_FILE.equals(currentSection)) { File f = resolveFile(projectDir, trimmed); if (f != null && f.exists() && f.isFile()) { - result.add(f); + activeFile = f; } } } - log.info(LOG_SOURCE, "Session loaded: " + result.size() + " file(s) to reopen"); + log.info(LOG_SOURCE, "Session loaded: " + openFiles.size() + " file(s), active=" + + (activeFile != null ? activeFile.getName() : "none")); } catch (IOException e) { log.error(LOG_SOURCE, "Failed to load session: " + e.getMessage()); } - return result; + return new SessionData(openFiles, activeFile); } private File resolveFile(File projectDir, String path) { diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 37a0518..551b66d 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -434,3 +434,6 @@ editor.menu.insertCode=Insérer un extrait de code menu.view.readingMode=Mode lecture reading.mode.exit=Quitter le mode lecture >>>>>>> main +# Workspace restore overlay (default/fallback) +restore.overlay.title=Restoring workspace… +restore.overlay.file=Opening {0} diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index 09f8112..79cabed 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -437,6 +437,10 @@ git.operation.fetch=Fetch\u2026 menu.view.readingMode=Lesemodus reading.mode.exit=Lesemodus beenden +# Overlay zur Arbeitsbereich-Wiederherstellung +restore.overlay.title=Arbeitsbereich wird wiederhergestellt… +restore.overlay.file=Öffne {0} + # Update-Prüfung menu.help.checkUpdate=Nach Updates suchen... update.available.title=Neue Version verfügbar diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index eb023c7..de6ce77 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -439,6 +439,10 @@ git.operation.fetch=Fetching\u2026 menu.view.readingMode=Enter Reading Mode reading.mode.exit=Exit Reading Mode +# Workspace restore overlay +restore.overlay.title=Restoring workspace… +restore.overlay.file=Opening {0} + # Update check menu.help.checkUpdate=Check for New Version... update.available.title=New Version Available diff --git a/src/main/resources/i18n/messages_es.properties b/src/main/resources/i18n/messages_es.properties index bfb4a2d..4c33129 100644 --- a/src/main/resources/i18n/messages_es.properties +++ b/src/main/resources/i18n/messages_es.properties @@ -437,6 +437,10 @@ git.operation.fetch=Recuperando\u2026 menu.view.readingMode=Modo lectura reading.mode.exit=Salir del modo lectura +# Superposición de restauración del espacio de trabajo +restore.overlay.title=Restaurando el espacio de trabajo… +restore.overlay.file=Abriendo {0} + # Comprobación de actualizaciones menu.help.checkUpdate=Buscar nueva versión... update.available.title=Nueva versión disponible @@ -449,4 +453,3 @@ update.uptodate.title=Actualizado update.uptodate.content=Está utilizando la última versión de MarkNote ({0}). update.error.title=Error al comprobar actualizaciones update.error.content=No se pudo comprobar si hay actualizaciones. Inténtelo de nuevo. - diff --git a/src/main/resources/i18n/messages_fr.properties b/src/main/resources/i18n/messages_fr.properties index 91c5dac..4e53205 100644 --- a/src/main/resources/i18n/messages_fr.properties +++ b/src/main/resources/i18n/messages_fr.properties @@ -440,6 +440,11 @@ git.operation.fetch=R\u00e9cup\u00e9rer\u2026 menu.view.readingMode=Mode lecture reading.mode.exit=Quitter le mode lecture + +# Overlay de restauration du workspace +restore.overlay.title=Restauration de l'espace de travail… +restore.overlay.file=Ouverture de {0} + # Vérification de mise à jour menu.help.checkUpdate=Vérifier les mises à jour... update.available.title=Nouvelle version disponible diff --git a/src/main/resources/i18n/messages_it.properties b/src/main/resources/i18n/messages_it.properties index 4525866..f91922c 100644 --- a/src/main/resources/i18n/messages_it.properties +++ b/src/main/resources/i18n/messages_it.properties @@ -436,6 +436,10 @@ git.operation.fetch=Aggiornamento in corso\u2026 menu.view.readingMode=Modalit\u00e0 lettura reading.mode.exit=Esci dalla modalit\u00e0 lettura +# Overlay di ripristino dell'area di lavoro +restore.overlay.title=Ripristino dell'area di lavoro… +restore.overlay.file=Apertura di {0} + # Controllo aggiornamenti menu.help.checkUpdate=Verifica nuova versione... update.available.title=Nuova versione disponibile