diff --git a/ivette/src/dome/doc/guides/application.md b/ivette/src/dome/doc/guides/application.md index ae191b062b31701c1d63c0ba49e4ad50526d7761..484a3c959757acde19a11f3730bba6c6885e087e 100644 --- a/ivette/src/dome/doc/guides/application.md +++ b/ivette/src/dome/doc/guides/application.md @@ -98,15 +98,22 @@ your data flow. `Dome.useEvent()` hooks can be used to make your components being notified by events. -- **Window Settings** are stored in the user's home directory but remain - generally unnoticed by most users, although they are responsible for a good user - experience. They typically include the window's position and dimension, - resizable items position, fold/unfold states, presentation options, etc. Most +- **Window Settings** are stored a local file at the root of user's project, and + remains generally unnoticed by most users. They typically include the window's + position and dimension, resizable items position, fold/unfold states, + presentation options, etc. Most **Dome** components with presentation options can be assigned a `settings` key to make their state persistent. Contrary to Global Settings, however, they are not shared across several windows. You may also access these data by using `Settings.setWindowSetting()` and `Settings.getWindowSetting()`, or the **React** hook `Settings.useWindowSetting()`. See also helpers `Dome.useXxxSettings()`. + It is possible, from the application main menu, to reset all the window settings to their + default values. + +- **Local Storage** are stored in the same file than window settings, although + they are not automatically reset to their initial values. + This is very convenient to store persistent user data on a per-project basis. + See `Settings.xxxLocalStorage()` functions for more details. - **Global Settings** are stored in the user's home directory and automatically saved and load with your application; they are typically modified _via_ the diff --git a/ivette/src/dome/src/main/dome.js b/ivette/src/dome/src/main/dome.js index 8b7d3c81195fc04ece1783978380f1034a04f2df..c53b6cf4afddce8f18d13bb8d08b7046fe77d968 100644 --- a/ivette/src/dome/src/main/dome.js +++ b/ivette/src/dome/src/main/dome.js @@ -107,6 +107,7 @@ function obtainGlobalSettings() { config: path; // Path to config file frame: { x,y,w,h }; // Frame position settings: object; // Current settings + storage: object; // Local storage reload: boolean; // Reloaded window } */ @@ -114,18 +115,20 @@ function obtainGlobalSettings() { const WindowHandles = {}; // Indexed by *webContents* id function saveWindowConfig(handle) { - const settings = { + const configData = { frame: handle.frame, settings: handle.settings, + storage: handle.storage, devtools: handle.devtools }; - saveSettings( handle.config, settings ); + saveSettings( handle.config, configData ); } function windowSyncSettings(event) { const handle = WindowHandles[event.sender.id]; event.returnValue = { globals: obtainGlobalSettings(), + storage: handle && handle.storage, settings: handle && handle.settings }; } @@ -154,6 +157,14 @@ function applyWindowSettings(event,args) { } } +function applyStorageSettings(event,args) { + const handle = WindowHandles[event.sender.id]; + if (handle) { + applyPatches( handle.storage, args ); + if (DEVEL) saveWindowConfig( handle ); + } +} + function applyGlobalSettings(event,args) { applyPatches( obtainGlobalSettings(), args ); BrowserWindow.getAllWindows().forEach((w) => { @@ -166,6 +177,7 @@ function applyGlobalSettings(event,args) { ipcMain.on('dome.ipc.settings.window', applyWindowSettings ); ipcMain.on('dome.ipc.settings.global', applyGlobalSettings ); +ipcMain.on('dome.ipc.settings.storage', applyStorageSettings ); // -------------------------------------------------------------------------- // --- Renderer-Process Communication @@ -294,7 +306,7 @@ function createBrowserWindow( config, argv, wdir ) const configFile = isAppWindow ? lookupConfig( wdir ) : PATH_WINDOW_SETTINGS ; const configData = loadSettings( configFile ); - const { frame, devtools, settings={} } = configData; + const { frame, devtools, settings={}, storage={} } = configData; if (frame) { const getInt = (v) => v && _.toSafeInteger(v); options.x = getInt(frame.x); @@ -309,7 +321,7 @@ function createBrowserWindow( config, argv, wdir ) const handle = { window: theWindow, config: configFile, - frame, settings, devtools, + frame, settings, storage, devtools, reload: false }; diff --git a/ivette/src/dome/src/renderer/data/settings.ts b/ivette/src/dome/src/renderer/data/settings.ts index 6caf4ee61109c181fc0aa69b899803ee8298610b..c0aeb7b61a25c9f1b9c0dde5096076bb991dc4a8 100644 --- a/ivette/src/dome/src/renderer/data/settings.ts +++ b/ivette/src/dome/src/renderer/data/settings.ts @@ -117,21 +117,24 @@ export class GObject<A extends JSON.json> extends GlobalSettings<A> { type store = { [key: string]: JSON.json }; type patch = { key: string; value: JSON.json }; -type driver = { evt: string; ipc: string; broadcast: boolean }; +type driver = { + evt: string; + ipc: string; + globals: boolean; // Global Settings (all windows share the same) + defaults: boolean; // Restore defaults on demand +}; class Driver { - readonly evt: string; // broadcast event - readonly broadcast: boolean; // settings broadcast + readonly evt: string; // Global Update Event readonly store: Map<string, JSON.json> = new Map(); readonly diffs: Map<string, JSON.json> = new Map(); - readonly fire: (() => void) & { flush: () => void; cancel: () => void }; + readonly commit: (() => void) & { flush: () => void; cancel: () => void }; - constructor({ evt, ipc, broadcast }: driver) { + constructor({ evt, ipc, defaults, globals }: driver) { this.evt = evt; - this.broadcast = broadcast; // --- Update Events - this.fire = debounce(() => { + this.commit = debounce(() => { const m = this.diffs; if (m.size > 0) { const patches: patch[] = []; @@ -143,14 +146,16 @@ class Driver { } }, 100); // --- Restore Defaults Events - ipcRenderer.on('dome.ipc.settings.defaults', () => { - this.fire.cancel(); - this.store.clear(); - this.diffs.clear(); - SysEmitter.emit(this.evt); - }); + if (defaults) { + ipcRenderer.on('dome.ipc.settings.defaults', () => { + this.commit.cancel(); + this.store.clear(); + this.diffs.clear(); + SysEmitter.emit(this.evt); + }); + } // --- Broadcast Events - if (this.broadcast) { + if (globals) { ipcRenderer.on( 'dome.ipc.settings.broadcast', (_sender, updates: patch[]) => { @@ -171,15 +176,15 @@ class Driver { } // --- Closing Events ipcRenderer.on('dome.ipc.closing', () => { - this.fire(); - this.fire.flush(); + this.commit(); + this.commit.flush(); }); } // --- Initial Data sync(data: store) { - this.fire.cancel(); + this.commit.cancel(); this.store.clear(); this.diffs.clear(); const m = this.store; @@ -204,8 +209,8 @@ class Driver { this.store.set(key, data); this.diffs.set(key, data); } - if (this.broadcast) SysEmitter.emit(this.evt); - this.fire(); + SysEmitter.emit(this.evt); + this.commit(); } } @@ -225,7 +230,7 @@ function useSettings<A>( ); // Local state const [value, setValue] = React.useState<A>(loader); - // Broadcast + // Emit update event React.useEffect(() => { const event = D.evt; const callback = () => setValue(loader()); @@ -249,7 +254,8 @@ function useSettings<A>( const WindowSettingsDriver = new Driver({ evt: 'dome.settings.window', ipc: 'dome.ipc.settings.window', - broadcast: false, + globals: false, + defaults: true, }); /** @@ -330,6 +336,78 @@ export function offWindowSettings(callback: () => void) { SysEmitter.off(evt, callback); } +// -------------------------------------------------------------------------- +// --- Local Storage +// -------------------------------------------------------------------------- + +const LocalStorageDriver = new Driver({ + evt: 'dome.settings.storage', + ipc: 'dome.ipc.settings.storage', + globals: false, + defaults: false, +}); + +/** + Returns the current value of the settings (default for undefined key). + */ +export function getLocalStorage<A>( + key: string | undefined, + decoder: JSON.Loose<A>, + defaultValue: A, +): A { + return key ? + JSON.jCatch(decoder, defaultValue)(LocalStorageDriver.load(key)) + : defaultValue; +} + +/** + Updates the current value of the settings (on defined key). + Most settings are subtypes of `JSON` and do not require any specific + encoder. If you have some, simply use it before updating the settings. + See [[useLocalStorage]] and [[useWindowsettingsdata]]. + */ +export function setLocalStorage( + key: string | undefined, + value: JSON.json, +) { + if (key) LocalStorageDriver.save(key, value); +} + +export function useLocalStorage<A extends JSON.json>( + key: string | undefined, + decoder: JSON.Loose<A>, + defaultValue: A, +) { + return useSettings({ + decoder, + encoder: JSON.identity, + defaultValue, + }, LocalStorageDriver, key); +} + +/** Same as [[useLocalStorage]] with a specific encoder. */ +export function useLocalStorageData<A>( + key: string | undefined, + decoder: JSON.Loose<A>, + encoder: JSON.Encoder<A>, + defaultValue: A, +) { + return useSettings({ + decoder, + encoder, + defaultValue, + }, LocalStorageDriver, key); +} + +/** Call the callback function on window settings events. */ +export function useLocalStorageEvent(callback: () => void) { + React.useEffect(() => { + const { evt } = LocalStorageDriver; + SysEmitter.on(evt, callback); + return () => { SysEmitter.off(evt, callback); }; + }); +} + // -------------------------------------------------------------------------- // --- Global Settings // -------------------------------------------------------------------------- @@ -337,7 +415,8 @@ export function offWindowSettings(callback: () => void) { const GlobalSettingsDriver = new Driver({ evt: 'dome.settings.global', ipc: 'dome.ipc.settings.global', - broadcast: true, + globals: true, + defaults: true, }); /** @@ -371,9 +450,11 @@ export const global = GlobalSettingsDriver.evt; /* @ internal */ export function synchronize() { const data = ipcRenderer.sendSync('dome.ipc.settings.sync'); - const globals: store = data.store ?? {}; - GlobalSettingsDriver.sync(globals); + const storage: store = data.storage ?? {}; + const globals: store = data.globals ?? {}; const settings: store = data.settings ?? {}; + LocalStorageDriver.sync(storage); + GlobalSettingsDriver.sync(globals); WindowSettingsDriver.sync(settings); }