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 @@
+