From e853aca82cb73cdde5fee4d833219a06e6cf6522 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Meyer Date: Mon, 18 May 2026 20:49:02 +0200 Subject: [PATCH 1/5] feat: restore workspace on startup (issue #82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persist and restore complete workspace state across sessions and hard kills: - Window geometry (x, y, width, height, maximized, fullscreen) saved to config via debounced property listeners — survives SIGKILL because state is written on every change, not only at shutdown - Active tab per project stored in [active_file] section of .marknote - ProjectSessionService: new SessionData record (openFiles + activeFile); loadSession() returns SessionData instead of List - Auto-restore on startup via handleStartupRestore(): when restoreWorkspaceOnStart=true (default), the last project + session are reopened automatically without a confirmation dialog - ShutdownHook registered for SIGTERM / unexpected JVM exit - geometryDebouncer shut down cleanly in Application.stop() Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/MarkNote.java | 158 +++++++++++++++--- src/main/java/config/AppConfig.java | 53 ++++++ .../java/utils/ProjectSessionService.java | 61 ++++--- 3 files changed, 227 insertions(+), 45 deletions(-) diff --git a/src/main/java/MarkNote.java b/src/main/java/MarkNote.java index 1fe4316..41d77b9 100644 --- a/src/main/java/MarkNote.java +++ b/src/main/java/MarkNote.java @@ -102,6 +102,7 @@ public class MarkNote extends Application { private GitService gitService; private ProjectSessionService projectSessionService; private Debouncer previewDebouncer; + private Debouncer geometryDebouncer; // LLM Chat Panel private PromptPanel promptPanel; @@ -213,12 +214,16 @@ public void start(Stage stage) { // Project session service projectSessionService = new ProjectSessionService(); - // Sauvegarder la session à chaque ajout ou suppression d'onglet de document + 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(); @@ -386,6 +391,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) { @@ -397,33 +407,24 @@ 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(); - } + handleStartupRestore(); }); splash.show(); } else { stage.show(); - handleReopenLastProject(); - if (config.isShowWelcomePage()) { - showWelcomeTab(); - } - if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) { - addNewDocument(); - } + handleStartupRestore(); } } @@ -1215,11 +1216,18 @@ private void openProjectDirectory(File dir) { refreshRecentMenu(); loadOrBuildIndex(dir); - // Rouvrir les documents de la session précédente - List sessionFiles = projectSessionService.loadSession(dir); - for (File f : sessionFiles) { + // Rouvrir les documents de la session précédente et restaurer l'onglet actif + 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)); + } } /** @@ -1376,6 +1384,107 @@ 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 sans dialogue. Sinon, le comportement historique est conservé. + */ + private void handleStartupRestore() { + if (config.isRestoreWorkspaceOnStart() && !config.getRecentDirs().isEmpty()) { + File lastDir = new File(config.getRecentDirs().getFirst()); + if (lastDir.exists() && lastDir.isDirectory()) { + openProjectDirectory(lastDir); + return; + } + } + // Comportement historique : dialogue de confirmation ou onglet de bienvenue + handleReopenLastProject(); + if (config.isShowWelcomePage()) { + showWelcomeTab(); + } + if (config.isOpenDocOnStart() && !config.isShowWelcomePage()) { + addNewDocument(); + } + } + + /** + * 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 est différé après show() car certaines plateformes l'ignorent avant + if (config.isWindowFullscreen()) { + 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) { + stage.xProperty().addListener((obs, o, n) -> { + if (!stage.isMaximized() && !stage.isFullScreen()) { + geometryDebouncer.debounce(() -> Platform.runLater(() -> { + config.setWindowX(stage.getX()); + config.save(); + })); + } + }); + stage.yProperty().addListener((obs, o, n) -> { + if (!stage.isMaximized() && !stage.isFullScreen()) { + geometryDebouncer.debounce(() -> Platform.runLater(() -> { + config.setWindowY(stage.getY()); + config.save(); + })); + } + }); + stage.widthProperty().addListener((obs, o, n) -> { + if (!stage.isMaximized() && !stage.isFullScreen()) { + geometryDebouncer.debounce(() -> Platform.runLater(() -> { + config.setWindowWidth(stage.getWidth()); + config.save(); + })); + } + }); + stage.heightProperty().addListener((obs, o, n) -> { + if (!stage.isMaximized() && !stage.isFullScreen()) { + geometryDebouncer.debounce(() -> Platform.runLater(() -> { + config.setWindowHeight(stage.getHeight()); + config.save(); + })); + } + }); + stage.maximizedProperty().addListener((obs, o, maximized) -> { + config.setWindowMaximized(maximized); + config.save(); + }); + stage.fullScreenProperty().addListener((obs, o, fullscreen) -> { + config.setWindowFullscreen(fullscreen); + config.save(); + }); + } + /** * Affiche le dialogue d'options. */ @@ -1581,7 +1690,9 @@ private void saveProjectSession() { .map(t -> ((DocumentTab) t).getFile()) .filter(f -> f.toPath().startsWith(projectDir.toPath())) .toList(); - projectSessionService.saveSession(projectDir, openFiles); + DocumentTab activeDocTab = getActiveDocumentTab(); + File activeFile = (activeDocTab != null && activeDocTab.getFile() != null) ? activeDocTab.getFile() : null; + projectSessionService.saveSession(projectDir, openFiles, activeFile); } @Override @@ -1591,6 +1702,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 013d586..e4123d6 100644 --- a/src/main/java/config/AppConfig.java +++ b/src/main/java/config/AppConfig.java @@ -39,6 +39,17 @@ public class AppConfig { 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; + public record PanelState(boolean visible, boolean docked, String zone) {} /** @@ -96,6 +107,20 @@ 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("panelState=")) { String raw = line.substring("panelState=".length()).trim(); String[] parts = raw.split("\\|", -1); @@ -138,6 +163,13 @@ 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); for (Map.Entry entry : panelStates.entrySet()) { PanelState state = entry.getValue(); lines.add("panelState=" + entry.getKey() + "|" + state.visible() + "|" + state.docked() + "|" + state.zone()); @@ -349,4 +381,25 @@ 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; } } 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) { From 5f2af503965a1ddbcaf667e32bc56082dd06afac Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Meyer Date: Mon, 18 May 2026 20:53:41 +0200 Subject: [PATCH 2/5] fix: skip window geometry save while in reading mode In reading mode the window is fullscreen and panels are rearranged. Persisting that state (fullscreen=true, resized dimensions) would cause the workspace to restore into a broken fullscreen layout on next launch. All six geometry listeners (x, y, width, height, maximized, fullscreen) now guard on !readingModeActive so only the pre-reading-mode geometry is kept in config. Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/MarkNote.java | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/MarkNote.java b/src/main/java/MarkNote.java index 41d77b9..8bc6e5d 100644 --- a/src/main/java/MarkNote.java +++ b/src/main/java/MarkNote.java @@ -1443,8 +1443,10 @@ private void restoreWindowGeometry(Stage stage) { * d'écrire la config à chaque pixel lors d'un redimensionnement. */ private void installWindowGeometryListeners(Stage stage) { + // En reading mode, toute la géométrie est ignorée : le fullscreen et les + // dimensions résultent du mode lecture, pas de la préférence utilisateur. stage.xProperty().addListener((obs, o, n) -> { - if (!stage.isMaximized() && !stage.isFullScreen()) { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) { geometryDebouncer.debounce(() -> Platform.runLater(() -> { config.setWindowX(stage.getX()); config.save(); @@ -1452,7 +1454,7 @@ private void installWindowGeometryListeners(Stage stage) { } }); stage.yProperty().addListener((obs, o, n) -> { - if (!stage.isMaximized() && !stage.isFullScreen()) { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) { geometryDebouncer.debounce(() -> Platform.runLater(() -> { config.setWindowY(stage.getY()); config.save(); @@ -1460,7 +1462,7 @@ private void installWindowGeometryListeners(Stage stage) { } }); stage.widthProperty().addListener((obs, o, n) -> { - if (!stage.isMaximized() && !stage.isFullScreen()) { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) { geometryDebouncer.debounce(() -> Platform.runLater(() -> { config.setWindowWidth(stage.getWidth()); config.save(); @@ -1468,7 +1470,7 @@ private void installWindowGeometryListeners(Stage stage) { } }); stage.heightProperty().addListener((obs, o, n) -> { - if (!stage.isMaximized() && !stage.isFullScreen()) { + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) { geometryDebouncer.debounce(() -> Platform.runLater(() -> { config.setWindowHeight(stage.getHeight()); config.save(); @@ -1476,12 +1478,16 @@ private void installWindowGeometryListeners(Stage stage) { } }); stage.maximizedProperty().addListener((obs, o, maximized) -> { - config.setWindowMaximized(maximized); - config.save(); + if (!readingModeActive) { + config.setWindowMaximized(maximized); + config.save(); + } }); stage.fullScreenProperty().addListener((obs, o, fullscreen) -> { - config.setWindowFullscreen(fullscreen); - config.save(); + if (!readingModeActive) { + config.setWindowFullscreen(fullscreen); + config.save(); + } }); } From f6ca9a4b0ae7b6a4ce41dde9f16badeed8657324 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Meyer Date: Mon, 18 May 2026 21:01:36 +0200 Subject: [PATCH 3/5] feat: add workspace restore overlay with progress bar Show an elegant semi-transparent overlay during startup workspace restoration so the user gets visual feedback while files are loaded. - WorkspaceRestoreOverlay: StackPane with 50% dark backdrop, centered card (white/rounded/drop-shadow), app logo, title label, determinate ProgressBar and per-file status label - Scene root changed to StackPane(root, overlay) so the overlay can cover the full window without disrupting existing layout - openSessionFilesSequentially(): opens session files one-by-one via Platform.runLater chain so the FX thread can repaint the progress bar between each file; hides overlay once all files are open - setupProjectDirectory() extracted from openProjectDirectory() to allow the restore path to call setup + sequential open separately - i18n keys restore.overlay.title / restore.overlay.file added to all 6 language files (fr, en, de, es, it, default) Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/MarkNote.java | 72 ++++++++--- src/main/java/ui/WorkspaceRestoreOverlay.java | 118 ++++++++++++++++++ src/main/resources/i18n/messages.properties | 3 + .../resources/i18n/messages_de.properties | 4 + .../resources/i18n/messages_en.properties | 4 + .../resources/i18n/messages_es.properties | 4 + .../resources/i18n/messages_fr.properties | 4 + .../resources/i18n/messages_it.properties | 4 + 8 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 src/main/java/ui/WorkspaceRestoreOverlay.java diff --git a/src/main/java/MarkNote.java b/src/main/java/MarkNote.java index 8bc6e5d..d7d2593 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 java.util.List; @@ -61,6 +62,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Screen; @@ -90,6 +92,7 @@ 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; @@ -383,7 +386,11 @@ public void start(Stage stage) { } }); - Scene scene = new Scene(root, 1200, 700); + // L'overlay est empilé au-dessus du root via un StackPane pour couvrir toute la fenêtre + restoreOverlay = new WorkspaceRestoreOverlay(messages); + StackPane sceneRoot = new StackPane(root, restoreOverlay); + + Scene scene = new Scene(sceneRoot, 1200, 700); applyTheme(scene); if (projectExplorerPanel.getProjectDirectory() == null) { stage.setTitle(messages.getString("app.title.editor")); @@ -1203,20 +1210,7 @@ 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 - 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 et restaurer l'onglet actif + setupProjectDirectory(dir); ProjectSessionService.SessionData session = projectSessionService.loadSession(dir); for (File f : session.openFiles()) { openFileInTab(f); @@ -1230,6 +1224,23 @@ private void openProjectDirectory(File dir) { } } + /** + * 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); + } + /** * Charge l'index existant ou en construit un nouveau pour le projet. * L'indexation complète s'exécute dans un thread séparé. @@ -1387,13 +1398,17 @@ 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 sans dialogue. Sinon, le comportement historique est conservé. + * 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()) { - openProjectDirectory(lastDir); + restoreOverlay.show(); + setupProjectDirectory(lastDir); + ProjectSessionService.SessionData session = projectSessionService.loadSession(lastDir); + openSessionFilesSequentially(session.openFiles(), 0, session.activeFile()); return; } } @@ -1407,6 +1422,29 @@ private void handleStartupRestore() { } } + /** + * 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. diff --git a/src/main/java/ui/WorkspaceRestoreOverlay.java b/src/main/java/ui/WorkspaceRestoreOverlay.java new file mode 100644 index 0000000..f2ad221 --- /dev/null +++ b/src/main/java/ui/WorkspaceRestoreOverlay.java @@ -0,0 +1,118 @@ +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; + +/** + * 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; + + setStyle("-fx-background-color: rgba(0,0,0,0.50);"); + setAlignment(Pos.CENTER); + setMaxWidth(Double.MAX_VALUE); + setMaxHeight(Double.MAX_VALUE); + setPickOnBounds(true); // bloque les clics pendant le chargement + + // ── Carte centrale ──────────────────────────────────────────── + VBox card = new VBox(14); + card.setAlignment(Pos.CENTER); + card.setPadding(new Insets(36, 52, 36, 52)); + card.setMaxWidth(420); + 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(360); + titleLabel.setAlignment(Pos.CENTER); + + // Barre de progression + progressBar = new ProgressBar(ProgressBar.INDETERMINATE_PROGRESS); + progressBar.setPrefWidth(320); + 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(360); + statusLabel.setAlignment(Pos.CENTER); + + if (logo != null) { + card.getChildren().addAll(logo, titleLabel, progressBar, statusLabel); + } else { + card.getChildren().addAll(titleLabel, progressBar, statusLabel); + } + getChildren().add(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/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 8659f20..e6cd066 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -436,3 +436,7 @@ git.operation.fetch=Fetch\u2026 # Lesemodus menu.view.readingMode=Lesemodus reading.mode.exit=Lesemodus beenden + +# Overlay zur Arbeitsbereich-Wiederherstellung +restore.overlay.title=Arbeitsbereich wird wiederhergestellt… +restore.overlay.file=Öffne {0} diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index d55647f..e831e84 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -438,3 +438,7 @@ git.operation.fetch=Fetching\u2026 # Reading mode 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} diff --git a/src/main/resources/i18n/messages_es.properties b/src/main/resources/i18n/messages_es.properties index 6d0cc59..80fa279 100644 --- a/src/main/resources/i18n/messages_es.properties +++ b/src/main/resources/i18n/messages_es.properties @@ -437,3 +437,7 @@ 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} diff --git a/src/main/resources/i18n/messages_fr.properties b/src/main/resources/i18n/messages_fr.properties index 4cf0adb..77b97fb 100644 --- a/src/main/resources/i18n/messages_fr.properties +++ b/src/main/resources/i18n/messages_fr.properties @@ -439,3 +439,7 @@ git.operation.fetch=R\u00e9cup\u00e9rer\u2026 # Mode lecture 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} diff --git a/src/main/resources/i18n/messages_it.properties b/src/main/resources/i18n/messages_it.properties index 26ba6dd..65fae24 100644 --- a/src/main/resources/i18n/messages_it.properties +++ b/src/main/resources/i18n/messages_it.properties @@ -436,3 +436,7 @@ 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} From 5195c9a302e22f004cd45751d03e339da5442f56 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Meyer Date: Mon, 18 May 2026 21:44:50 +0200 Subject: [PATCH 4/5] Fix workspace restore overlay coverage and window geometry persistence - Replace StackPane with AnchorPane as scene root so the overlay is pinned to all 4 edges and always covers the full window - Replace programmatic setBackground() with a Rectangle child whose width/height are bound to the overlay, making it immune to CSS theme overrides - Fix geometry debouncer bug: all 4 listeners (x, y, width, height) now share a single saveGeometry Runnable so a resize cannot cause one property to silently overwrite the others mid-debounce - Apply macOS fullscreen restoration via showingProperty listener instead of bare Platform.runLater (fires too early before stage show) Co-Authored-By: Claude Sonnet 4.6 --- src/main/java/MarkNote.java | 80 +++++++++++-------- src/main/java/ui/WorkspaceRestoreOverlay.java | 23 +++--- 2 files changed, 60 insertions(+), 43 deletions(-) diff --git a/src/main/java/MarkNote.java b/src/main/java/MarkNote.java index d7d2593..0d67a47 100644 --- a/src/main/java/MarkNote.java +++ b/src/main/java/MarkNote.java @@ -62,7 +62,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; +import javafx.scene.layout.AnchorPane; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Screen; @@ -99,7 +99,6 @@ public class MarkNote extends Application { private DockingManager dockingManager; private ConsolePanel consolePanel; - private CheckMenuItem showConsoleMenuItem; private boolean consoleDebugEnabled = false; private GitService gitService; @@ -386,9 +385,18 @@ public void start(Stage stage) { } }); - // L'overlay est empilé au-dessus du root via un StackPane pour couvrir toute la fenêtre + // L'overlay recouvre toute la fenêtre via un AnchorPane (ancres 0 sur les 4 côtés) restoreOverlay = new WorkspaceRestoreOverlay(messages); - StackPane sceneRoot = new StackPane(root, restoreOverlay); + 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); @@ -537,7 +545,7 @@ private MenuBar createMenuBar() { // 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()); @@ -1465,11 +1473,20 @@ private void restoreWindowGeometry(Stage stage) { if (config.isWindowMaximized()) { stage.setMaximized(true); } - // Le fullscreen est différé après show() car certaines plateformes l'ignorent avant + // Le fullscreen doit être appliqué après que la fenêtre soit affichée (macOS green button) if (config.isWindowFullscreen()) { - Platform.runLater(() -> { - stage.setFullScreenExitHint(""); - stage.setFullScreen(true); + 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); + }); + } + } }); } } @@ -1481,39 +1498,34 @@ private void restoreWindowGeometry(Stage stage) { * d'écrire la config à chaque pixel lors d'un redimensionnement. */ private void installWindowGeometryListeners(Stage stage) { - // En reading mode, toute la géométrie est ignorée : le fullscreen et les - // dimensions résultent du mode lecture, pas de la préférence utilisateur. - stage.xProperty().addListener((obs, o, n) -> { + // 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()) { - geometryDebouncer.debounce(() -> Platform.runLater(() -> { - config.setWindowX(stage.getX()); - config.save(); - })); + 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(() -> Platform.runLater(() -> { - config.setWindowY(stage.getY()); - config.save(); - })); - } + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) + geometryDebouncer.debounce(saveGeometry); }); stage.widthProperty().addListener((obs, o, n) -> { - if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) { - geometryDebouncer.debounce(() -> Platform.runLater(() -> { - config.setWindowWidth(stage.getWidth()); - config.save(); - })); - } + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) + geometryDebouncer.debounce(saveGeometry); }); stage.heightProperty().addListener((obs, o, n) -> { - if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) { - geometryDebouncer.debounce(() -> Platform.runLater(() -> { - config.setWindowHeight(stage.getHeight()); - config.save(); - })); - } + if (!readingModeActive && !stage.isMaximized() && !stage.isFullScreen()) + geometryDebouncer.debounce(saveGeometry); }); stage.maximizedProperty().addListener((obs, o, maximized) -> { if (!readingModeActive) { diff --git a/src/main/java/ui/WorkspaceRestoreOverlay.java b/src/main/java/ui/WorkspaceRestoreOverlay.java index f2ad221..9fed583 100644 --- a/src/main/java/ui/WorkspaceRestoreOverlay.java +++ b/src/main/java/ui/WorkspaceRestoreOverlay.java @@ -13,6 +13,7 @@ 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. @@ -27,17 +28,21 @@ public class WorkspaceRestoreOverlay extends StackPane { public WorkspaceRestoreOverlay(ResourceBundle messages) { this.messages = messages; - setStyle("-fx-background-color: rgba(0,0,0,0.50);"); setAlignment(Pos.CENTER); - setMaxWidth(Double.MAX_VALUE); - setMaxHeight(Double.MAX_VALUE); 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(36, 52, 36, 52)); - card.setMaxWidth(420); + 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;" + @@ -52,12 +57,12 @@ public WorkspaceRestoreOverlay(ResourceBundle messages) { 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(360); + titleLabel.setMaxWidth(580); titleLabel.setAlignment(Pos.CENTER); // Barre de progression progressBar = new ProgressBar(ProgressBar.INDETERMINATE_PROGRESS); - progressBar.setPrefWidth(320); + progressBar.setPrefWidth(520); progressBar.setPrefHeight(8); progressBar.setStyle("-fx-accent: #3584e4;"); @@ -65,7 +70,7 @@ public WorkspaceRestoreOverlay(ResourceBundle messages) { statusLabel = new Label(" "); statusLabel.setStyle("-fx-font-size: 11px; -fx-text-fill: #6b7a8f;"); statusLabel.setWrapText(true); - statusLabel.setMaxWidth(360); + statusLabel.setMaxWidth(580); statusLabel.setAlignment(Pos.CENTER); if (logo != null) { @@ -73,7 +78,7 @@ public WorkspaceRestoreOverlay(ResourceBundle messages) { } else { card.getChildren().addAll(titleLabel, progressBar, statusLabel); } - getChildren().add(card); + getChildren().addAll(backdrop, card); setVisible(false); } From ca3ee4725d3c628a6ee42807764fe7dc3355b7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delorme?= Date: Mon, 18 May 2026 23:16:04 +0200 Subject: [PATCH 5/5] fiux(restore): accept PR: the reopen dialog will be fixed later --- src/main/java/MarkNote.java | 26 ++++++++++ src/main/java/config/AppConfig.java | 75 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/main/java/MarkNote.java b/src/main/java/MarkNote.java index eab0046..8a5223f 100644 --- a/src/main/java/MarkNote.java +++ b/src/main/java/MarkNote.java @@ -770,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) { @@ -1027,6 +1045,14 @@ 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(); } diff --git a/src/main/java/config/AppConfig.java b/src/main/java/config/AppConfig.java index 1b49f91..e9593da 100644 --- a/src/main/java/config/AppConfig.java +++ b/src/main/java/config/AppConfig.java @@ -33,6 +33,11 @@ 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 = ""; @@ -154,6 +159,35 @@ public void load() { 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) { + } + } + } } } } catch (IOException ignored) { @@ -196,6 +230,23 @@ public void save() { 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() + "|" @@ -500,4 +551,28 @@ public boolean isRestoreWorkspaceOnStart() { 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]; + } }