Skip to content

WIP: Save state of view split and windows size/position to settings.yaml #193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ try
build
public/katex/
public/paged.polyfill.js
public/highlight.js
9 changes: 7 additions & 2 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,14 @@ If the directory does not exist, you can create it.

## Settings

If you put a `settings.yaml` file in the data directory, PanWriter will read it on startup. Possible fields are currently only:
If you put a `settings.yaml` file in the data directory, PanWriter will read it on startup. Available settings are:

autoUpdateApp: true
autoUpdateApp: true # whether to automatically check for and install updates
extensions: # optional: configure markdown-it extensions
some-extension: true
other-extension: false

For available extension options and their effects, please refer to the [markdown-it-pandoc documentation](https://github.com/mb21/markdown-it-pandoc#readme).

## Default CSS and YAML

Expand Down
11 changes: 10 additions & 1 deletion electron/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { BrowserWindow, ipcMain, shell } from 'electron'
import { Doc } from '../src/appState/AppState'
import { Doc, ViewSplit } from '../src/appState/AppState'
import { readDataDirFile } from './dataDir'
import { Message } from './preload'
import { updateSettings } from './settings'

// this file contains the IPC functionality of the main process.
// for the renderer process's part see electron/preload.ts
Expand Down Expand Up @@ -31,6 +32,14 @@ export const init = () => {
const [ meta ] = await readDataDirFile(fileName)
return meta
})

ipcMain.handle('saveViewSplitState', async (_event, split: ViewSplit) => {
try {
await updateSettings({ viewSplitState: split })
} catch (err) {
console.error('Failed to save view split state:', err)
}
})
}

export const getDoc = async (win: BrowserWindow): Promise<Doc> => {
Expand Down
241 changes: 188 additions & 53 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,31 @@ import { importFile } from './pandoc/import'
import { saveFile, openFile } from './file'
import { Message } from './preload'
import { clearRecentFiles, getRecentFiles } from './recentFiles'
import { loadSettings } from './settings'
import { loadSettings, updateSettings } from './settings'

const { autoUpdater } = require('electron-updater')
require('fix-path')() // needed to execute pandoc on macOS prod build

let appWillQuit = false
const settingsPromise = loadSettings()

/**
* Simple debounce function to limit the rate at which a function can fire
* @param func The function to debounce
* @param wait The time to wait in milliseconds
* @returns A debounced function
*/
function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return function(this: any, ...args: Parameters<T>) {
const context = this;
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args);
}, wait);
};
}

declare class CustomBrowserWindow extends Electron.BrowserWindow {
wasCreatedOnStartup?: boolean;
Expand All @@ -31,109 +48,183 @@ const mdExtensions = ['md', 'txt', 'markdown']
ipc.init()

const createWindow = async (filePath?: string, toImport=false, wasCreatedOnStartup=false) => {
const win: CustomBrowserWindow = new BrowserWindow({
width: 1000
, height: 800
, frame: process.platform !== 'darwin'
, show: false
, webPreferences: {
nodeIntegration: false
, contextIsolation: true
, preload: __dirname + '/preload.js'
, sandbox: true
// Define default window options
const windowOptions: Electron.BrowserWindowConstructorOptions = {
width: 1000,
height: 800,
frame: process.platform !== 'darwin',
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: __dirname + '/preload.js',
sandbox: true
}
};

// Apply saved window bounds if available
const settings = await settingsPromise;
if (settings.windowBounds) {
const { width, height, x, y } = settings.windowBounds;

// Validate dimensions and position to prevent window from being off-screen
const displays = require('electron').screen.getAllDisplays();
let isValid = false;

for (const display of displays) {
const { workArea } = display;

// Check if window would be visible in this display
if (
x >= workArea.x && y >= workArea.y &&
x + width <= workArea.x + workArea.width &&
y + height <= workArea.y + workArea.height
) {
isValid = true;
break;
}
})
}

// Only apply saved bounds if they are valid
if (isValid) {
windowOptions.width = width;
windowOptions.height = height;
windowOptions.x = x;
windowOptions.y = y;
}
}

const win: CustomBrowserWindow = new BrowserWindow(windowOptions);

win.wasCreatedOnStartup = wasCreatedOnStartup;
win.setTitle('Untitled');

// Add event listeners to save window bounds
const saveBoundsDebounced = debounce(() => {
saveWindowBounds(win);
}, 500);

win.wasCreatedOnStartup = wasCreatedOnStartup
win.setTitle('Untitled')
win.on('resize', saveBoundsDebounced);
win.on('move', saveBoundsDebounced);

// close auto-created window when first user action is to open/import another file
windows.filter(w => w.wasCreatedOnStartup).forEach(async w => {
const { fileDirty } = await ipc.getDoc(w)
const { fileDirty } = await ipc.getDoc(w);
if (!fileDirty) {
w.close()
w.close();
}
})
});

windows.push(win)
windows.push(win);

const windowReady = new Promise<void>(resolve =>
win.once('ready-to-show', resolve)
)
);

const isDev = !!process.env.ELECTRON_IS_DEV
const isDev = !!process.env.ELECTRON_IS_DEV;
if (isDev) {
win.loadURL('http://localhost:3000/index.html')
win.loadURL('http://localhost:3000/index.html');
} else {
// win.loadFile('build/index.html')
win.loadURL(`file://${__dirname}/../index.html`)
win.loadURL(`file://${__dirname}/../index.html`);
}

if (isDev) {
win.webContents.openDevTools()
win.webContents.openDevTools();
}

if (filePath) {
const doc = toImport
? await importFile(win, filePath)
: await openFile(win, filePath)
const settings = await settingsPromise
: await openFile(win, filePath);
if (doc) {
await windowReady
ipc.sendMessage(win, { type: 'initDoc', doc, settings })
await windowReady;
ipc.sendMessage(win, { type: 'initDoc', doc, settings });

// If there's a saved split state, set it after a small delay to ensure it's applied
if (settings.viewSplitState &&
(settings.viewSplitState === 'onlyEditor' ||
settings.viewSplitState === 'split' ||
settings.viewSplitState === 'onlyPreview')) {
setTimeout(() => {
ipc.sendMessage(win, { type: 'split', split: settings.viewSplitState as 'onlyEditor' | 'split' | 'onlyPreview' });
}, 100);
}
} else {
ipc.sendMessage(win, { type: 'loadSettings', settings })
ipc.sendMessage(win, { type: 'loadSettings', settings });
}
} else {
await windowReady;
ipc.sendMessage(win, { type: 'loadSettings', settings });

// If there's a saved split state, set it after a small delay to ensure it's applied
if (settings.viewSplitState &&
(settings.viewSplitState === 'onlyEditor' ||
settings.viewSplitState === 'split' ||
settings.viewSplitState === 'onlyPreview')) {
setTimeout(() => {
ipc.sendMessage(win, { type: 'split', split: settings.viewSplitState as 'onlyEditor' | 'split' | 'onlyPreview' });
}, 100);
}
}
await windowReady
ipc.sendPlatform(win)
win.show()
setMenu()

// Apply maximized state if needed
if (settings.windowBounds?.isMaximized) {
win.maximize();
}

await windowReady;
ipc.sendPlatform(win);
win.show();
setMenu();

win.on('close', async e => {
// Save window bounds when closing
saveWindowBounds(win);

// this does not intercept a reload
// see https://github.com/electron/electron/blob/master/docs/api/browser-window.md#event-close
// and https://github.com/electron/electron/issues/9966
if (!win.dontPreventClose) {
e.preventDefault()
e.preventDefault();
const close = () => {
win.dontPreventClose = true
win.close()
win.dontPreventClose = true;
win.close();
if (appWillQuit) {
app.quit()
app.quit();
}
}
const doc = await ipc.getDoc(win)
};
const doc = await ipc.getDoc(win);
if (doc.fileDirty) {
const selected = await dialog.showMessageBox(win, {
type: "question"
, message: "This document has unsaved changes."
, buttons: ["Save", "Cancel", "Don't Save"]
})
});
switch (selected.response) {
case 0: {
// Save
win.dontPreventClose = true
await saveFile(win, doc)
close()
break
win.dontPreventClose = true;
await saveFile(win, doc);
close();
break;
}
case 1: {
// Cancel
appWillQuit = false
break
appWillQuit = false;
break;
}
case 2: {
// Don't Save
close()
break
close();
break;
}
}
} else {
close()
close();
}
}
})
});

win.on('closed', () => {
// Dereference the window so it can be garbage collected
Expand All @@ -143,7 +234,7 @@ const createWindow = async (filePath?: string, toImport=false, wasCreatedOnStart
}

setMenu(windows.length > 0, true);
})
});

win.on('minimize', () => {
if (windows.filter(w => !w.isMinimized()).length === 0) {
Expand Down Expand Up @@ -267,6 +358,39 @@ const windowSendMessage = async (msg: Message) => {
}
}

const saveSplitSetting = (split: string) => {
// Only save valid split settings
if (split === 'onlyEditor' || split === 'split' || split === 'onlyPreview') {
updateSettings({ viewSplitState: split })
.catch(err => console.error('Failed to save split setting:', err))
}
}

/**
* Saves the current window's bounds (position, size, and maximized state) to settings
* @param win The BrowserWindow instance to save bounds for
*/
async function saveWindowBounds(win: BrowserWindow) {
try {
// Get the current window bounds
const bounds = win.getNormalBounds();
const isMaximized = win.isMaximized();

// Update the settings with the new bounds
await updateSettings({
windowBounds: {
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
isMaximized
}
});
} catch (error) {
console.error('Failed to save window bounds:', error);
}
}

const setMenu = async (aWindowIsOpen=true, useRecentFilesCache=false) => {
const recentFiles = await getRecentFiles(useRecentFilesCache)
const template: Electron.MenuItemConstructorOptions[] = [
Expand Down Expand Up @@ -389,17 +513,26 @@ const setMenu = async (aWindowIsOpen=true, useRecentFilesCache=false) => {
, submenu: [
{ label: 'Show Only Editor'
, accelerator: 'CmdOrCtrl+1'
, click: () => windowSendMessage({ type: 'split', split: 'onlyEditor' })
, click: () => {
windowSendMessage({ type: 'split', split: 'onlyEditor' })
saveSplitSetting('onlyEditor')
}
, enabled: aWindowIsOpen
}
, { label: 'Show Split View'
, accelerator: 'CmdOrCtrl+2'
, click: () => windowSendMessage({ type: 'split', split: 'split' })
, click: () => {
windowSendMessage({ type: 'split', split: 'split' })
saveSplitSetting('split')
}
, enabled: aWindowIsOpen
}
, { label: 'Show Only Preview'
, accelerator: 'CmdOrCtrl+3'
, click: () => windowSendMessage({ type: 'split', split: 'onlyPreview' })
, click: () => {
windowSendMessage({ type: 'split', split: 'onlyPreview' })
saveSplitSetting('onlyPreview')
}
, enabled: aWindowIsOpen
}
, {type: 'separator'}
Expand Down Expand Up @@ -456,3 +589,5 @@ const setMenu = async (aWindowIsOpen=true, useRecentFilesCache=false) => {
var menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}


Loading