diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bedb879..ccae74b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,6 @@ -Adds support for update notifications. GitHub will be checked for updates on startup and a notification will be sent with a download link when a newer version is found. \ No newline at end of file +Improves handling of window state, including open tabs and selected fields. This data is preserved when opening a new log file or restarting the app. Other miscellaneous improvements include... + +* Tabs can now be closed in addition to opened! +* Only numbers are allowed as fields for odometry. +* Minor visual updates to the odometry popup button & tab bar. +* Lots of internal cleanup. \ No newline at end of file diff --git a/indexPreload.js b/indexPreload.js index 48a3e70c..84d900a4 100644 --- a/indexPreload.js +++ b/indexPreload.js @@ -14,6 +14,16 @@ ipcRenderer.on("set-focused", (_, isFocused) => { })) }) +ipcRenderer.on("restore-state", (_, state) => { + window.dispatchEvent(new CustomEvent("restore-state", { + detail: state + })) +}) + +window.addEventListener("save-state", event => { + ipcRenderer.send("save-state", event.detail) +}) + ipcRenderer.on("open-file", (_, path) => { fs.open(path, "r", function (err, file) { if (err) throw err diff --git a/main.js b/main.js index 82cc0129..4dbdebbd 100644 --- a/main.js +++ b/main.js @@ -1,9 +1,11 @@ const { app, BrowserWindow, Menu, MenuItem, shell, dialog, ipcMain } = require("electron") -const windowStateKeeper = require("electron-window-state") +const WindowStateKeeper = require("./windowState.js") const { setUpdateNotification } = require("electron-update-notifier") const path = require("path") const os = require("os") +const stateFileName = "state-" + app.getVersion().replaceAll(".", '_') + ".json" + var firstOpenPath = null app.whenReady().then(() => { setupMenu() @@ -66,10 +68,18 @@ function createWindow() { } // Manage window state - var windowState = windowStateKeeper({ + var window = null + var windowState = WindowStateKeeper({ + file: stateFileName, defaultWidth: 1100, defaultHeight: 650, - fullScreen: false + fullScreen: false, + saveDataHandler: saveStateHandler, + restoreDataHandler: state => { + window.once("ready-to-show", () => { + window.send("restore-state", state) + }) + } }) if (BrowserWindow.getFocusedWindow() == null) { prefs.x = windowState.x @@ -91,7 +101,7 @@ function createWindow() { } // Create window - const window = new BrowserWindow(prefs) + window = new BrowserWindow(prefs) windowState.manage(window) // Finish setup @@ -106,6 +116,17 @@ function createWindow() { return window } +// Manage state +var states = {} +ipcMain.on("save-state", (event, state) => { + states[event.sender.getOwnerBrowserWindow()] = state +}) + +function saveStateHandler(window) { + return states[window] +} + +// Create app menu function setupMenu() { const isMac = process.platform === "darwin" diff --git a/package.json b/package.json index 0d8d6e88..44cbf880 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "6328-log-viewer", "productName": "6328 Log Viewer", - "version": "1.0.1", + "version": "1.1.0", "description": "Logging tool from FRC Team 6328.", "main": "main.js", "scripts": { diff --git a/windowState.js b/windowState.js new file mode 100644 index 00000000..f1ecda1c --- /dev/null +++ b/windowState.js @@ -0,0 +1,191 @@ +// Modified from https://github.com/mawie81/electron-window-state + +const path = require('path'); +const electron = require('electron'); +const jsonfile = require('jsonfile'); +const mkdirp = require('mkdirp'); + +module.exports = function (options) { + const app = electron.app || electron.remote.app; + const screen = electron.screen || electron.remote.screen; + let state; + let winRef; + let stateChangeTimer; + const eventHandlingDelay = 100; + const config = Object.assign({ + file: 'window-state.json', + path: app.getPath('userData'), + maximize: true, + fullScreen: true, + saveDataHandler: () => null, + restoreDataHandler: () => null + }, options); + const fullStoreFileName = path.join(config.path, config.file); + + function isNormal(win) { + return !win.isMaximized() && !win.isMinimized() && !win.isFullScreen(); + } + + function hasBounds() { + return state && + Number.isInteger(state.x) && + Number.isInteger(state.y) && + Number.isInteger(state.width) && state.width > 0 && + Number.isInteger(state.height) && state.height > 0; + } + + function resetStateToDefault() { + const displayBounds = screen.getPrimaryDisplay().bounds; + + // Reset state to default values on the primary display + state = { + width: config.defaultWidth || 800, + height: config.defaultHeight || 600, + x: 0, + y: 0, + displayBounds, + data + }; + } + + function windowWithinBounds(bounds) { + return ( + state.x >= bounds.x && + state.y >= bounds.y && + state.x + state.width <= bounds.x + bounds.width && + state.y + state.height <= bounds.y + bounds.height + ); + } + + function ensureWindowVisibleOnSomeDisplay() { + const visible = screen.getAllDisplays().some(display => { + return windowWithinBounds(display.bounds); + }); + + if (!visible) { + // Window is partially or fully not visible now. + // Reset it to safe defaults. + return resetStateToDefault(); + } + } + + function validateState() { + const isValid = state && (hasBounds() || state.isMaximized || state.isFullScreen); + if (!isValid) { + state = null; + return; + } + + if (hasBounds() && state.displayBounds) { + ensureWindowVisibleOnSomeDisplay(); + } + } + + function updateState(win) { + win = win || winRef; + if (!win) { + return; + } + // Don't throw an error when window was closed + try { + const winBounds = win.getBounds(); + if (isNormal(win)) { + state.x = winBounds.x; + state.y = winBounds.y; + state.width = winBounds.width; + state.height = winBounds.height; + } + state.isMaximized = win.isMaximized(); + state.isFullScreen = win.isFullScreen(); + state.displayBounds = screen.getDisplayMatching(winBounds).bounds; + state.data = config.saveDataHandler(win); + } catch (err) { } + } + + function saveState(win) { + // Update window state only if it was provided + if (win) { + updateState(win); + } + + // Save state + try { + mkdirp.sync(path.dirname(fullStoreFileName)); + jsonfile.writeFileSync(fullStoreFileName, state); + } catch (err) { + // Don't care + } + } + + function stateChangeHandler() { + // Handles both 'resize' and 'move' + clearTimeout(stateChangeTimer); + stateChangeTimer = setTimeout(updateState, eventHandlingDelay); + } + + function closeHandler() { + updateState(); + } + + function closedHandler() { + // Unregister listeners and save state + unmanage(); + saveState(); + } + + function manage(win) { + if (config.maximize && state.isMaximized) { + win.maximize(); + } + if (config.fullScreen && state.isFullScreen) { + win.setFullScreen(true); + } + if (state.data) config.restoreDataHandler(state.data); + win.on('resize', stateChangeHandler); + win.on('move', stateChangeHandler); + win.on('close', closeHandler); + win.on('closed', closedHandler); + winRef = win; + } + + function unmanage() { + if (winRef) { + winRef.removeListener('resize', stateChangeHandler); + winRef.removeListener('move', stateChangeHandler); + clearTimeout(stateChangeTimer); + winRef.removeListener('close', closeHandler); + winRef.removeListener('closed', closedHandler); + winRef = null; + } + } + + // Load previous state + try { + state = jsonfile.readFileSync(fullStoreFileName); + } catch (err) { + // Don't care + } + + // Check state validity + validateState(); + + // Set state fallback values + state = Object.assign({ + width: config.defaultWidth || 800, + height: config.defaultHeight || 600 + }, state); + + return { + get x() { return state.x; }, + get y() { return state.y; }, + get width() { return state.width; }, + get height() { return state.height; }, + get displayBounds() { return state.displayBounds; }, + get isMaximized() { return state.isMaximized; }, + get isFullScreen() { return state.isFullScreen; }, + saveState, + unmanage, + manage, + resetStateToDefault + }; +}; \ No newline at end of file diff --git a/www/global.css b/www/global.css index c4387f9c..589edda2 100644 --- a/www/global.css +++ b/www/global.css @@ -10,11 +10,6 @@ body { } } -:root { - --side-bar-width: 300px; - --tab-control-inline: 0; -} - /* Buttons */ button { diff --git a/www/index.css b/www/index.css index 15402473..8f6c405b 100644 --- a/www/index.css +++ b/www/index.css @@ -1,3 +1,7 @@ +:root { + --tab-control-inline: 0; +} + /* Title bar layout */ div.title-bar { @@ -192,7 +196,7 @@ div.field-item-label { div.tab-bar { position: absolute; left: 10px; - right: calc(10px + var(--tab-control-inline) * 144px); + right: calc(10px + var(--tab-control-inline) * 172px); top: 0px; height: 50px; @@ -221,7 +225,7 @@ div.tab-bar-shadow-left { div.tab-bar-shadow-right { opacity: 100%; - right: calc(10px + var(--tab-control-inline) * 144px); + right: calc(10px + var(--tab-control-inline) * 172px); background-image: linear-gradient(to left, white, rgba(255, 255, 255, 0.75), transparent); } @@ -238,7 +242,7 @@ div.tab-bar-shadow-right { div.tab-bar-scroll { position: absolute; left: 10px; - right: calc(10px + var(--tab-control-inline) * 144px); + right: calc(10px + var(--tab-control-inline) * 172px); top: 0px; height: 50px; @@ -310,10 +314,14 @@ button.tab-control { } button.play, button.pause { - right: 116px; + right: 144px; } button.move-left { + right: 106px; +} + +button.close { right: 78px; } @@ -765,7 +773,7 @@ button.odometry-popup-button { position: absolute; width: 25px; height: 25px; - top: 0px; + top: 1px; right: 5px; } diff --git a/www/index.html b/www/index.html index fd130c62..2a4046c8 100644 --- a/www/index.html +++ b/www/index.html @@ -36,6 +36,12 @@ +