diff --git a/ivette/.dome-pkg-app.lock b/ivette/.dome-pkg-app.lock index 7282f0cc447a1caf3cb155d03008966d7b2f41ab..f9677904486a71fb448d4a6db632128c0d3013ac 100644 --- a/ivette/.dome-pkg-app.lock +++ b/ivette/.dome-pkg-app.lock @@ -1 +1 @@ -react@^16.8 react-dom source-map-support lodash react-virtualized react-draggable codemirror +react@^16.8 react-dom source-map-support lodash react-virtualized react-draggable react-fast-compare codemirror diff --git a/ivette/.gitignore b/ivette/.gitignore index da037a9e2fa9fa6eb676dcadabf0f8486c4b79bf..194fc7ea7ce2c5c7ffb30e127542107772220cd0 100644 --- a/ivette/.gitignore +++ b/ivette/.gitignore @@ -2,6 +2,7 @@ # --- Template .gitignore file for Dome # -------------------------------------------------------------------------- +.ivette .dome-*.stamp .dome-*.back node_modules diff --git a/ivette/package.json b/ivette/package.json index d77d420e12005ff11011b27dd7bca04d6531db44..c64081d369b41efd624bd2c93e58da8180866fa5 100644 --- a/ivette/package.json +++ b/ivette/package.json @@ -44,7 +44,6 @@ "eslint-plugin-react-hooks": "^3.0.0", "html-loader": "1.0.0-alpha.0", "jsdoc": "^3.6.3", - "react-fast-compare": "^3.2.0", "react-hot-loader": "^4.12.20", "serve": "^11.3.0", "typedoc": "^0.17.6", @@ -62,6 +61,7 @@ "react-draggable": "^4.2.0", "react-virtualized": "^9.21.2", "source-map-support": "^0.5.16", + "react-fast-compare": "^3.2.0", "zeromq": "^6.0.0-beta.5" } } diff --git a/ivette/src/dome/src/main/dome.js b/ivette/src/dome/src/main/dome.js index 1385eb6bc5e50f273d46a85201ae79d68e74e093..cd01363afb0b82961da1f77f1baed2b3865e5b7c 100644 --- a/ivette/src/dome/src/main/dome.js +++ b/ivette/src/dome/src/main/dome.js @@ -50,83 +50,136 @@ export const platform = System.platform ; // --- Settings // -------------------------------------------------------------------------- -const APP_DIR = app.getPath('userData'); -const APP_SETTINGS = path.join( APP_DIR , 'Settings.json' ); - -var s_frames = {} ; -var s_globals = {} ; -var s_application = {} ; -var s_preferences = {} ; +function loadSettings( file ) { + try { + if (!fstat(file)) + return {}; + const text = fs.readFileSync(file, { encoding: 'utf8' } ); + return Object.assign({}, JSON.parse(text)); + } catch(err) { + console.error("[Dome] Unable to load settings", file, err); + return {}; + } +} -function loadSettings() { +function saveSettings( file, data={} ) { try { - if (!fstat( APP_SETTINGS )) return; - const content = fs.readFileSync( APP_SETTINGS, { encoding: 'utf8' } ); - const loaded = JSON.parse( content ); - const MERGE = (store,field) => _.merge( store , _.get( loaded , field )); - s_frames = MERGE( s_frames , 'frames' ); - s_globals = MERGE( s_globals, 'globals' ); - s_application = MERGE( s_application, 'application' ); - s_preferences = MERGE( s_preferences, 'preferences' ); + const text = JSON.stringify( data, undefined, DEVEL ? 2 : 0 ); + fs.writeFileSync( file, text, { encoding: 'utf8' }, (err) => { throw(err); } ); } catch(err) { - console.error("[Dome] Can not load application settings\n" + err); + console.error("[Dome] Unable to save settings", file, err); } } -function saveSettings() { +// -------------------------------------------------------------------------- +// --- Global Settings +// -------------------------------------------------------------------------- + +var GlobalSettings; // Current Dictionnary + +const APP_DIR = app.getPath('userData'); +const PATH_WINDOW_SETTINGS = path.join( APP_DIR, 'WindowSettings.json' ); +const PATH_GLOBAL_SETTINGS = path.join( APP_DIR, 'GlobalSettings.json' ); + +function saveGlobalSettings() { try { if (!fstat( APP_DIR )) fs.mkdirSync( APP_DIR ); - const saved = { - globals: s_globals, - application: s_application, - preferences: s_preferences, - frames: s_frames - }; - const content = JSON.stringify( saved, undefined, DEVEL ? 2 : 0 ); - fs.writeFileSync( APP_SETTINGS, content, { encoding: 'utf8' }, errorSettings ); + saveSettings( PATH_GLOBAL_SETTINGS, GlobalSettings ); } catch(err) { - errorSettings(err); + console.error("[Dome] Unable to save global settings", err); } } -const fireSaveSettings = _.debounce( saveSettings , 50 ); +function obtainGlobalSettings() { + if (!GlobalSettings) { + GlobalSettings = loadSettings( PATH_GLOBAL_SETTINGS ); + } + return GlobalSettings; +} + +// -------------------------------------------------------------------------- +// --- Window Settings & Frames +// -------------------------------------------------------------------------- -function errorSettings(err) { - if (err) console.error("[Dome] Can not save application settings\n" + err); +/* Window Handle: + { + window: BrowserWindow ; // Also prevents Gc + config: path; // Path to config file + frame: { x,y,w,h }; // Frame position + settings: object; // Current settings + reload: boolean; // Reloaded window + } + */ + +const WindowHandles = {}; // Indexed by *webContents* id + +function saveWindowConfig(handle) { + const settings = { + frame: handle.frame, + settings: handle.settings, + devtools: handle.devtools + }; + saveSettings( handle.config, settings ); } -function remoteSyncSettings(event) -{ - const isSetting = windowSettings && windowSettings.id === event.frameId ; +function windowSyncSettings(event) { + const handle = WindowHandles[event.sender.id]; event.returnValue = { - globals: s_globals, - settings: isSetting ? s_preferences : s_application + globals: obtainGlobalSettings(), + settings: handle && handle.settings }; } -function remoteSaveWindowSettings(event,patches) -{ - const isSetting = windowSettings && windowSettings.id === event.frameId ; - _.merge( isSetting ? s_preferences : s_application , patches ); - saveSettings(); +ipcMain.on('dome.ipc.settings.sync', windowSyncSettings ); + +// -------------------------------------------------------------------------- +// --- Patching Settings +// -------------------------------------------------------------------------- + +function applyPatches( data, args ) { + args.forEach(({ key, value }) => { + if (value === null) { + delete data[key]; + } else { + data[key] = value; + } + }); } -function remoteSaveGlobalSettings(event,patches) -{ - _.merge( s_globals , patches ); - saveSettings(); - BrowserWindow.getAllWindows().forEach((win) => { - if (win.id !== event.frameId) - win.send('dome.ipc.settings.update',patches); +function applyWindowSettings(event,args) { + const handle = WindowHandles[event.sender.id]; + if (handle) { + applyPatches( handle.settings, args ); + if (DEVEL) saveWindowConfig( handle ); + } +} + +function applyGlobalSettings(event,args) { + applyPatches( obtainGlobalSettings(), args ); + BrowserWindow.getAllWindows().forEach((w) => { + if (w.webContents.id !== event.sender.id) { + w.send('dome.ipc.settings.update',args); + } }); + if (DEVEL) saveGlobalSettings(); } -ipcMain.on('dome.ipc.settings.sync', remoteSyncSettings ); -ipcMain.on('dome.ipc.settings.window', remoteSaveWindowSettings ); -ipcMain.on('dome.ipc.settings.global', remoteSaveGlobalSettings ); +ipcMain.on('dome.ipc.settings.window', applyWindowSettings ); +ipcMain.on('dome.ipc.settings.global', applyGlobalSettings ); // -------------------------------------------------------------------------- -// --- Active Windows +// --- Renderer-Process Communication +// -------------------------------------------------------------------------- + +function broadcast( event, ...args ) +{ + BrowserWindow.getAllWindows().forEach((w) => { + w.send( event, ...args ); + }); +} + +// -------------------------------------------------------------------------- +// --- Window Activities // -------------------------------------------------------------------------- var appName = 'Dome' ; @@ -141,21 +194,24 @@ export function setName(title) { } function setTitle(event,title) { - let w = BrowserWindow.fromId( event.frameId ); - w.setTitle( title || appName ); + let handle = WindowHandles[event.sender.id]; + handle && handle.setTitle( title || appName ); } function setModified(event,modified) { - let w = BrowserWindow.frameId( event.frameId ); - if (platform == 'macos') - w.setDocumentEdited( modified ); - else { - let title = w.getTitle(); - if (title.startsWith(MODIFIED)) - title = title.substring(MODIFIED.length); - if (modified) - title = MODIFIED + title ; - w.setTitle(title); + let handle = WindowHandles[event.sender.id]; + if (handle) { + const w = handle.window; + if (platform == 'macos') + w.setDocumentEdited( modified ); + else { + let title = w.getTitle(); + if (title.startsWith(MODIFIED)) + title = title.substring(MODIFIED.length); + if (modified) + title = MODIFIED + title ; + w.setTitle(title); + } } } @@ -192,34 +248,55 @@ function navigateURL( event , url ) { } // -------------------------------------------------------------------------- -// --- Browser Window SetUp +// --- Lookup for config file // -------------------------------------------------------------------------- -const windowsHandle = {} ; // Prevent live windows to be garbage collected -const windowsReload = {} ; // Reloaded windows +function lookupConfig(wdir) { + let cwd = wdir = path.resolve(wdir); + let cfg = '.' + appName.toLowerCase(); + for(;;) { + const here = path.join(cwd,cfg); + if (fstat(here)) return here; + let up = path.dirname(cwd); + if (up === cwd) break; + cwd = up; + } + const home = path.resolve(app.getPath('home')); + const user = wdir.startsWith(home) ? wdir : home ; + return path.join( user, cfg ); +} + +// -------------------------------------------------------------------------- +// --- Browser Window SetUp +// -------------------------------------------------------------------------- -function createBrowserWindow( config, isMain=true ) +function createBrowserWindow( config, argv, wdir ) { - const argv = isMain + const isAppWindow = (argv !== undefined && wdir !== undefined); + + const browserArguments = isAppWindow ? SYS.WINDOW_APPLICATION_ARGV : SYS.WINDOW_PREFERENCES_ARGV ; - const options = _.merge( + const options = Object.assign( { show: false, backgroundColor: '#f0f0f0', webPreferences: { nodeIntegration:true, - additionalArguments: [ argv ] + additionalArguments: [ browserArguments ] } - } - , config ); + }, + config + ); - const frameId = isMain ? 'application' : 'preferences' ; - const frame = _.get( s_frames, frameId ); - const getInt = (v) => v && _.toSafeInteger(v); + const configFile = isAppWindow ? lookupConfig( wdir ) : PATH_WINDOW_SETTINGS ; + const configData = loadSettings( configFile ); + + const { frame, devtools, settings={} } = configData; if (frame) { + const getInt = (v) => v && _.toSafeInteger(v); options.x = getInt(frame.x); options.y = getInt(frame.y); options.width = getInt(frame.width); @@ -227,8 +304,25 @@ function createBrowserWindow( config, isMain=true ) } const theWindow = new BrowserWindow( options ); - const wid = theWindow.id; - + const wid = theWindow.webContents.id; + + const handle = { + window: theWindow, + config: configFile, + frame, settings, devtools, + reload: false + }; + + // Keep the window reference (prevent garbage collection) + WindowHandles[wid] = handle; + + // Emitted when the window is closed. + theWindow.on('closed', () => { + saveWindowConfig(handle); + // Dereference the window object (allow garbage collection) + delete WindowHandles[wid] ; + }); + // Load the index.html of the app. if (DEVEL || LOCAL) process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'; @@ -239,7 +333,7 @@ function createBrowserWindow( config, isMain=true ) theWindow.once('ready-to-show' , () => { if (DEVEL || LOCAL) process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'false'; - if (DEVEL) + if (DEVEL && devtools) theWindow.openDevTools(); theWindow.show(); }); @@ -251,57 +345,44 @@ function createBrowserWindow( config, isMain=true ) // URL Navigation theWindow.webContents.on('will-navigate', navigateURL ); theWindow.webContents.on('did-navigate-in-page', navigateURL ); + + // Application Startup theWindow.webContents.on('did-finish-load', () => { - const isLoaded = windowsReload[wid]; - if (!isLoaded) { - windowsReload[wid] = true; + if (!handle.reload) { + handle.reload = true; } else { broadcast('dome.ipc.reload'); } + theWindow.send('dome.ipc.command',argv,wdir); }); // Emitted when the window want's to close. theWindow.on('close', (evt) => { + handle.frame = theWindow.getBounds(); + handle.devtools = theWindow.isDevToolsOpened(); theWindow.send('dome.ipc.closing'); - const frame = theWindow.getBounds(); - _.set( s_frames, frameId , frame ); }); // Keep track of frame positions (in DEVEL) if (DEVEL) { - const reframe = _.debounce( (evt) => { - const frame = theWindow.getBounds(); - _.set( s_frames, frameId , frame ); - saveSettings(); + const saveFrame = _.debounce( (evt) => { + handle.frame = theWindow.getBounds(); + handle.devtools = theWindow.isDevToolsOpened(); + saveWindowConfig(handle); } , 300); - theWindow.on('resize',reframe); - theWindow.on('moved',reframe); + theWindow.on('resize',saveFrame); + theWindow.on('moved',saveFrame); } - // Keep the window reference to prevent destruction - windowsHandle[ wid ] = theWindow ; - - // Emitted when the window is closed. - theWindow.on('closed', () => { - // Dereference the window object to actually destroy it - delete windowsHandle[ wid ] ; - }); - return theWindow ; } // -------------------------------------------------------------------------- -// --- Application Window(s) +// --- Application Window(s) & Command Line // -------------------------------------------------------------------------- -function filterArgv( argv ) { - return argv.slice( DEVEL ? 3 : (LOCAL ? 2 : 1) ).filter((p) => p); -} - -function sendCommand( win, argv, wdir ) { - win.webContents.on('did-finish-load', () => { - win.webContents.send('dome.ipc.command',argv,wdir); - }); +function stripElectronArgv( argv ) { + return argv.slice( DEVEL ? 3 : (LOCAL ? 2 : 1) ).filter((p) => !!p); } function createPrimaryWindow() @@ -309,31 +390,28 @@ function createPrimaryWindow() // Initialize Menubar Menubar.install(); - // Initialize Settings - loadSettings(); - // React Developper Tools if (DEVEL) installExtension(REACT_DEVELOPER_TOOLS,true); - - const primary = createBrowserWindow({ title: appName } , true); - const wdir = process.cwd() === '/' ? app.getPath('home') : process.cwd() ; - sendCommand( primary , filterArgv(process.argv) , wdir ); + const cwd = process.cwd(); + const wdir = cwd === '/' ? app.getPath('home') : cwd ; + const argv = stripElectronArgv(process.argv); + createBrowserWindow({ title: appName } , argv, wdir ); } var appCount = 1; -function createSecondaryWindow(_event,argv,wdir) +function createSecondaryWindow(_event,process_argv,wdir) { - const secondary = createBrowserWindow({ title: `${appName} #${++appCount}` }, true); - sendCommand( secondary, filterArgv(argv), wdir ); + const argv = stripElectronArgv(process_argv); + createBrowserWindow({ title: `${appName} #${++appCount}` }, argv, wdir); } function createDesktopWindow() { const instance = appCount++ ; - const secondary = createBrowserWindow({ title: `${appName} #${++appCount}` }, true); - sendCommand( secondary , [] , app.getPath('home') ); + const wdir = app.getPath('home'); + createBrowserWindow({ title: `${appName} #${++appCount}` }, [], wdir); } // -------------------------------------------------------------------------- @@ -361,31 +439,35 @@ function activateWindows() { // --- Settings Window // -------------------------------------------------------------------------- -var windowSettings = undefined ; // Preference Window +var PreferenceWindow = undefined ; // Preference Window function showSettingsWindow() { - if (!windowSettings) - windowSettings = createBrowserWindow({ + if (!PreferenceWindow) + PreferenceWindow = createBrowserWindow({ title: appName + ' Settings', width: 256, height: 248, fullscreen: false, maximizable: false, minimizable: false - }, false); - windowSettings.show(); - windowSettings.on('closed',() => windowSettings = undefined); + }); + PreferenceWindow.show(); + PreferenceWindow.on('closed',() => PreferenceWindow = undefined); } function restoreDefaultSettings() { - s_globals = {} ; - s_preferences = {} ; - s_application = {} ; - s_frames = {} ; - fireSaveSettings(); - fireSaveSettings.flush(); + GlobalSettings = {}; + if (DEVEL) saveGlobalSettings(); + + _.forEach( WindowHandles, (handle) => { + // Keep frame for user comfort + handle.settings = {}; + handle.devtools = handle.window.isDevToolsOpened(); + if (DEVEL) saveWindowConfig(handle); + }); + broadcast( 'dome.ipc.settings.defaults' ); } @@ -402,22 +484,21 @@ export function start() { // Change default locale app.commandLine.appendSwitch('lang','en'); - // Listen to window events + // Listen to application events app.on( 'ready', createPrimaryWindow ); // Wait for Electron init app.on( 'activate', activateWindows ); // Mac OSX response to dock app.on( 'second-instance', createSecondaryWindow ); app.on( 'dome.menu.settings', showSettingsWindow ); app.on( 'dome.menu.defaults', restoreDefaultSettings ); - // Performing on-exit callbacks + // At-exit callbacks app.on( 'will-quit' , () => { + saveGlobalSettings(); System.doExit() ; - fireSaveSettings(); - fireSaveSettings.flush(); }); - // On OS X menu bar stay active until the user quits explicitly from menu. - // On other systems, quit when all windows are closed. + // On macOS the menu bar stays active until the user explicitly quits. + // On other systems, automatically quit when all windows are closed. // Warning: when no event handler is registered, the app automatically // quit when all windows are closed. app.on( 'window-all-closed', () => { @@ -426,17 +507,6 @@ export function start() { } -// -------------------------------------------------------------------------- -// --- Renderer-Process Communication -// -------------------------------------------------------------------------- - -function broadcast( event, ...args ) -{ - BrowserWindow.getAllWindows().forEach((w) => { - w.send( event, ...args ); - }); -} - // -------------------------------------------------------------------------- // --- MenuBar Management // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/misc/plugins.js b/ivette/src/dome/src/misc/plugins.js index 5d468d4017a308f65f0dfad93f3ef2dcc6c17aea..87a08c0c836e960d70158825db1a98a685f2282c 100644 --- a/ivette/src/dome/src/misc/plugins.js +++ b/ivette/src/dome/src/misc/plugins.js @@ -34,7 +34,7 @@ export function install( name ) let config ; try { config = JSON.pargse(fs.readFileSync( pkg , 'UTF-8' )); } catch(err) { - console.error( `[Dome] reading '${pkg}':\n`, err ); + console.error( `[Dome] Reading '${pkg}':\n`, err ); throw `Plugin '${name}' has invalid 'package.json' file` ; } @@ -49,7 +49,7 @@ export function install( name ) let bundle ; try { bundle = fs.readFileSync( bundlejs , 'UTF-8' ); } catch(err) { - console.error( `[Dome] loading '${bundlejs}':\n`, err ); + console.error( `[Dome] Loading '${bundlejs}':\n`, err ); throw `Plugin '${name}' can not load its entry point` ; } @@ -63,7 +63,7 @@ export function install( name ) let module = { id, exports }; compiled( module, require, static_d ); } catch(err) { - console.error( `[Dome] running '${bundlejs}':\n`, err ); + console.error( `[Dome] Running '${bundlejs}':\n`, err ); throw `Plugin '${name}' can not install bundle` ; } register( id, exports ); // final exports diff --git a/ivette/src/dome/src/misc/system.js b/ivette/src/dome/src/misc/system.js index 48808f72562593c59043fcd977d147a4f5abee93..3a4deac841f74817b387562192b72de5108edd91 100644 --- a/ivette/src/dome/src/misc/system.js +++ b/ivette/src/dome/src/misc/system.js @@ -593,7 +593,7 @@ export function spawn(command,args,options) { } if ( !process ) { - throw "[Dome] Can not create process ('"+command+"')"; + throw `[Dome] Unable to create process ('${command}')`; return; } diff --git a/ivette/src/dome/src/renderer/controls/buttons.tsx b/ivette/src/dome/src/renderer/controls/buttons.tsx index da254148990e172ace57336361370f2a9fb42bf1..0789962f9c941cebc455ec5c53ea11d0b2cce94d 100644 --- a/ivette/src/dome/src/renderer/controls/buttons.tsx +++ b/ivette/src/dome/src/renderer/controls/buttons.tsx @@ -161,8 +161,6 @@ export function Button(props: ButtonProps) { + (display ? '' : ' dome-control-erased') + (className ? ' ' + className : ''); const nofocus = focusable ? undefined : true; - console.log('ICON', Icon); - console.log('LABEL', LABEL); return ( <button type='button' className={theClass} diff --git a/ivette/src/dome/src/renderer/data/compare.ts b/ivette/src/dome/src/renderer/data/compare.ts index 6dcbec1d018875f4d3eceff7f1eafb345d04b700..5f643b4cc8598d834e39ed8eb593616f6345294d 100644 --- a/ivette/src/dome/src/renderer/data/compare.ts +++ b/ivette/src/dome/src/renderer/data/compare.ts @@ -8,6 +8,8 @@ @module dome/data/compare */ +import FastCompare from 'react-fast-compare'; + /** Interface for comparison functions. These function shall fullfill the following contract: @@ -22,11 +24,19 @@ export interface Order<A> { (x: A, y: A): number; } +/** + Deep structural equality. + Provided by [react-fast-compare](). +*/ +export const isEqual = FastCompare; + +/** Always returns 0. */ export function equal(_x: any, _y: any): 0 { return 0; } +/** Primitive comparison works on this type. */ export type bignum = bigint | number; -/** Non-NaN numbers and big-ints */ +/** Detect Non-NaN numbers and big-ints. */ export function isBigNum(x: any): x is bignum { return typeof (x) === 'bigint' || (typeof (x) === 'number' && !Number.isNaN(x)); } @@ -222,6 +232,34 @@ export function byAllFields<A>(order: ByAllFields<A>): Order<A> { }; } +export type dict<A> = undefined | null | { [key: string]: A }; + +/** + Compare dictionaries _wrt_ lexicographic order of entries. +*/ +export function dictionary<A>(order: Order<A>): Order<dict<A>> { + return (x: dict<A>, y: dict<A>) => { + if (x === y) return 0; + const dx = x ?? {}; + const dy = y ?? {}; + const phi = option(order); + const fs = Object.getOwnPropertyNames(dx).sort(); + const gs = Object.getOwnPropertyNames(dy).sort(); + const p = fs.length; + const q = gs.length; + for (let i = 0, j = 0; i < p && j < q;) { + let a = undefined, b = undefined; + const f = fs[i]; + const g = gs[j]; + if (f <= g) { a = dx[f]; i++; } + if (g <= f) { b = dy[g]; j++; } + const cmp = phi(a, b); + if (cmp != 0) return cmp; + } + return p - q; + }; +} + /** Pair comparison. */ export function pair<A, B>(ordA: Order<A>, ordB: Order<B>): Order<[A, B]> { return (u, v) => { diff --git a/ivette/src/dome/src/renderer/data/json.ts b/ivette/src/dome/src/renderer/data/json.ts new file mode 100644 index 0000000000000000000000000000000000000000..4c1d36796e57d508ffef52d80abc2810e21e8744 --- /dev/null +++ b/ivette/src/dome/src/renderer/data/json.ts @@ -0,0 +1,422 @@ +// -------------------------------------------------------------------------- +// --- JSON Utilities +// -------------------------------------------------------------------------- + +/** + Safe JSON utilities. + @packageDocumentation + @module dome/data/json +*/ + +import { DEVEL } from 'dome/system'; + +export type json = + undefined | null | number | string | json[] | { [key: string]: json } + +/** + Parse without _revivals_. + Returned data is guaranteed to have only [[json]] type. + If an error occurs and `noError` is set to `true`, + the function returns `undefined` and logs the error in console + (DEVEL mode only). + */ +export function parse(text: string, noError = false): json { + if (noError) { + try { + return JSON.parse(text); + } catch (err) { + if (DEVEL) console.error('[Dome.json] Invalid format:', err); + return undefined; + } + } else + return JSON.parse(text); +} + +/** + Export JSON (or any data) as a compact string. +*/ +export function stringify(js: any) { + return JSON.stringify(js, undefined, 0); +} + +/** + Export JSON (or any data) as a string with indentation. + */ +export function pretty(js: any) { + return JSON.stringify(js, undefined, 2); +} + +// -------------------------------------------------------------------------- +// --- SAFE Decoder +// -------------------------------------------------------------------------- + +/** Decoder for values of type `D`. + You can abbreviate `Safe<D | undefined>` with `Loose<D>`. */ +export type Safe<D> = (js?: json) => D; + +/** Decoder for values of type `D`, if any. + Same as `Safe<D | undefined>`. */ +export type Loose<D> = (js?: json) => D | undefined; + +/** + Encoder for value of type `D`. + In most cases, you only need [[identity]]. + */ +export type Encoder<D> = (v: D) => json; + +/** Can be used for most encoders. */ +export function identity<A>(v: A): A { return v; }; + +// -------------------------------------------------------------------------- +// --- Primitives +// -------------------------------------------------------------------------- + +/** Primitive JSON number or `undefined`. */ +export const jNumber: Loose<number> = (js: json) => ( + typeof js === 'number' && !Number.isNaN(js) ? js : undefined +); + +/** Primitive JSON number, rounded to integer, or `undefined`. */ +export const jInt: Loose<number> = (js: json) => ( + typeof js === 'number' && !Number.isNaN(js) ? Math.round(js) : undefined +); + +/** Primitive JSON number or `0`. */ +export const jZero: Safe<number> = (js: json) => ( + typeof js === 'number' && !Number.isNaN(js) ? js : 0 +); + +/** Primitive JSON boolean or `undefined`. */ +export const jBoolean: Loose<boolean> = (js: json) => ( + typeof js === 'boolean' ? js : undefined +); + +/** Primitive JSON boolean or `true`. */ +export const jTrue: Safe<boolean> = (js: json) => ( + typeof js === 'boolean' ? js : true +); + +/** Primitive JSON boolean or `false`. */ +export const jFalse: Safe<boolean> = (js: json) => ( + typeof js === 'boolean' ? js : false +); + +/** Primitive JSON string or `undefined`. */ +export const jString: Loose<string> = (js: json) => ( + typeof js === 'string' ? js : undefined +); + +/** + One of the enumerated _constants_ or `undefined`. + The typechecker will prevent you from listing values that are not in + type `A`. However, it will not protect you from missings constants in `A`. +*/ +export function jEnum<A>(...values: ((string | number) & A)[]): Loose<A> { + var m = new Map<string | number, A>(); + values.forEach(v => m.set(v, v)); + return (v: json) => (typeof v === 'string' ? m.get(v) : undefined); +} + +/** + Refine a loose decoder with some default value. + The default value is returned when the provided JSON is `undefined` or + when the loose decoder returns `undefined`. + */ +export function jDefault<A>( + fn: Loose<A>, + defaultValue: A, +): Safe<A> { + return (js: json) => + js === undefined ? defaultValue : (fn(js) ?? defaultValue); +} + +/** + Force returning `undefined` or a default value for `undefined` JSON input. + Typically useful to leverage an existing `Safe<A>` decoder. + */ +export function jOption<A>(fn: Safe<A>, defaultValue?: A): Loose<A> { + return (js: json) => (js === undefined ? defaultValue : fn(js)); +} + +/** + Force returning `undefined` or a default value for `undefined` _or_ `null` + JSON input. Typically useful to leverage an existing `Safe<A>` decoder. + */ +export function jNull<A>(fn: Safe<A>, defaultValue?: A): Loose<A> { + return (js: json) => (js === undefined || js === null ? defaultValue : fn(js)); +} + +/** + Fail when the loose decoder returns `undefined`. + See also [[jCatch]] and [[jTry]]. + */ +export function jFail<A>(fn: Loose<A>, error: Error): Safe<A> { + return (js: json) => { + const d = fn(js); + if (d !== undefined) return d; + throw error; + }; +} + +/** + Provide a fallback value in case of undefined value or error. + See also [[jFail]] and [[jTry]]. + */ +export function jCatch<A>(fn: Loose<A>, fallBack: A): Safe<A> { + return (js: json) => { + try { + return fn(js) ?? fallBack; + } catch (err) { + if (DEVEL) console.error('[Dome.json]', err); + return fallBack; + } + }; +} + +/** + Provides an (optional) default value in case of error or undefined value. + See also [[jFail]] and [[jCatch]]. + */ +export function jTry<A>(fn: Loose<A>, defaultValue?: A): Loose<A> { + return (js: json) => { + try { + return fn(js) ?? defaultValue; + } catch (_err) { + return defaultValue; + } + }; +} + +/** + Converts maps to dictionaries. + */ +export function jMap<A>(fn: Loose<A>): Safe<Map<string, A>> { + return (js: json) => { + const m = new Map<string, A>(); + if (js !== null && typeof js === 'object' && !Array.isArray(js)) { + for (let k of Object.keys(js)) { + const v = fn(js[k]); + if (v !== undefined) m.set(k, v); + } + } + return m; + }; +} + +/** + Converts dictionaries to maps. + */ +export function eMap<A>(fn: Encoder<A>): Encoder<Map<string, undefined | A>> { + return m => { + const js: json = {}; + m.forEach((v, k) => { + if (v !== undefined) { + const u = fn(v); + if (u !== undefined) js[k] = u; + } + }); + return js; + }; +} + +/** + Apply the decoder on each item of a JSON array, or return `[]` otherwise. + Can be also applied on a _loose_ decoder, but you will get + an array with possibly `undefined` elements. Use [[jList]] + to discard undefined elements, or use a true _safe_ decoder. + */ +export function jArray<A>(fn: Safe<A>): Safe<A[]> { + return (js: json) => Array.isArray(js) ? js.map(fn) : []; +} + +/** + Apply the loose decoder on each item of a JSON array, discarding + all `undefined` elements. To keep the all possibly undefined array entries, + use [[jArray]] instead. + */ +export function jList<A>(fn: Loose<A>): Safe<A[]> { + return (js: json) => { + const buffer: A[] = []; + if (Array.isArray(js)) js.forEach(vj => { + const d = fn(vj); + if (d !== undefined) buffer.push(d); + }); + return buffer; + }; +} + +/** + Exports all non-undefined elements. + */ +export function eList<A>(fn: Encoder<A>): Encoder<(A | undefined)[]> { + return m => { + const js: json[] = []; + m.forEach(v => { + if (v !== undefined) { + const u = fn(v); + if (u !== undefined) js.push(u); + } + }); + return js; + }; +} + +/** Apply a pair of decoders to JSON pairs, or return `undefined`. */ +export function jPair<A, B>( + fa: Safe<A>, + fb: Safe<B>, +): Loose<[A, B]> { + return (js: json) => Array.isArray(js) ? [ + fa(js[0]), + fb(js[1]), + ] : undefined; +} + +/** Similar to [[jPair]]. */ +export function jTriple<A, B, C>( + fa: Safe<A>, + fb: Safe<B>, + fc: Safe<C>, +): Loose<[A, B, C]> { + return (js: json) => Array.isArray(js) ? [ + fa(js[0]), + fb(js[1]), + fc(js[2]), + ] : undefined; +} + +/** Similar to [[jPair]]. */ +export function jTuple4<A, B, C, D>( + fa: Safe<A>, + fb: Safe<B>, + fc: Safe<C>, + fd: Safe<D>, +): Loose<[A, B, C, D]> { + return (js: json) => Array.isArray(js) ? [ + fa(js[0]), + fb(js[1]), + fc(js[2]), + fd(js[3]), + ] : undefined; +} + +/** Similar to [[jPair]]. */ +export function jTuple5<A, B, C, D, E>( + fa: Safe<A>, + fb: Safe<B>, + fc: Safe<C>, + fd: Safe<D>, + fe: Safe<E>, +): Loose<[A, B, C, D, E]> { + return (js: json) => Array.isArray(js) ? [ + fa(js[0]), + fb(js[1]), + fc(js[2]), + fd(js[3]), + fe(js[4]), + ] : undefined; +} + +/** + Decoders for each property of object type `A`. + Optional fields in `A` can be assigned a loose decoder. +*/ +export type Props<A> = { + [P in keyof A]: Safe<A[P]>; +} + +/** + Decode an object given the decoders of its fields. + Returns `undefined` for non-object JSON. + */ +export function jObject<A>(fp: Props<A>): Loose<A> { + return (js: json) => { + if (js !== null && typeof js === 'object' && !Array.isArray(js)) { + const buffer = {} as A; + for (var k of Object.keys(fp)) { + const fn = fp[k as keyof A]; + if (fn !== undefined) { + const fj = js[k]; + if (fj !== undefined) { + const fv = fn(fj); + if (fv !== undefined) buffer[k as keyof A] = fv; + } + } + } + return buffer; + } + return undefined; + }; +} + +/** + Encoders for each property of object type `A`. +*/ +export type EProps<A> = { + [P in keyof A]?: Encoder<A[P]>; +} + +/** + Encode an object given the provided encoders by fields. + The exported JSON object has only original + fields with some specified encoder. + */ +export function eObject<A>(fp: EProps<A>): Encoder<A> { + return (m: A) => { + const js: json = {}; + for (var k of Object.keys(fp)) { + const fn = fp[k as keyof A]; + if (fn !== undefined) { + const fv = m[k as keyof A]; + if (fv !== undefined) { + const r = fn(fv); + if (r !== undefined) js[k] = r; + } + } + } + return js; + } +} + +/** Type of dictionaries. */ +export type dict<A> = { [key: string]: A }; + +/** + Decode a JSON dictionary, discarding all inconsistent entries. + If the JSON contains no valid entry, still returns `{}`. +*/ +export function jDictionary<A>(fn: Loose<A>): Safe<dict<A>> { + return (js: json) => { + const buffer: dict<A> = {}; + if (js !== null && typeof js === 'object' && !Array.isArray(js)) { + for (var k of Object.keys(js)) { + const fd = js[k]; + if (fd !== undefined) { + const fv = fn(fd); + if (fv !== undefined) buffer[k] = fv; + } + } + } + return buffer; + }; +} + +/** + Encode a dictionary into JSON, discarding all inconsistent entries. + If the dictionary contains no valid entry, still returns `{}`. +*/ +export function eDictionary<A>(fn: Encoder<A>): Encoder<dict<A>> { + return (d: dict<A>) => { + const js: json = {}; + for (var k of Object.keys(d)) { + const fv = d[k]; + if (fv !== undefined) { + const fv = fn(d[k]); + if (fv !== undefined) js[k] = fv; + } + } + return js; + }; +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/data/states.ts b/ivette/src/dome/src/renderer/data/states.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9e0de2a648e5d5fee1d0081ada3a9d4f58f7308 --- /dev/null +++ b/ivette/src/dome/src/renderer/data/states.ts @@ -0,0 +1,328 @@ +// -------------------------------------------------------------------------- +// --- States +// -------------------------------------------------------------------------- + +/** + Typed States & Settings + @packageDocumentation + @module dome/data/states +*/ + +import React from 'react'; +import isEqual from 'react-fast-compare'; +import { DEVEL } from 'dome/misc/system'; +import * as Dome from 'dome'; +import * as JSON from './json'; + +export type NonFunction = + undefined | null | boolean | number | string + | object | any[] | bigint | symbol; + +/** State updater. New value or updating function applied to the current, + lastly updated value. Use `null` to restore default value. */ +export type updateAction<A extends NonFunction> = + null | A | ((current: A) => A); + +/** The type of updater callbacks. Typically used for `[A, setState<A>]` + hooks. */ +export type setState<A extends NonFunction> = (action: updateAction<A>) => void; + +/** Base state interface. */ +export interface State<A extends NonFunction> { + readonly get: () => A; + readonly set: (value: A) => void; + readonly update: setState<A>; + on(callback: (value: A) => void): void; + off(callback: (value: A) => void): void; +} + +/** React Hook, similar to `React.useState()`. */ +export function useState<A extends NonFunction>(s: State<A>): [A, setState<A>] { + const [current, setCurrent] = React.useState<A>(s.get); + React.useEffect(() => { + s.on(setCurrent); + return () => s.off(setCurrent); + }); + return [current, s.update]; +}; + +/** + State with initial default value. + */ +export class StateDef<A extends NonFunction> implements State<A> { + protected value: A; + protected defaultValue: A; + protected event: symbol; + + constructor(defaultValue: A) { + this.value = this.defaultValue = defaultValue; + this.event = Symbol('dome.state'); + this.get = this.get.bind(this); + this.set = this.get.bind(this); + this.reset = this.reset.bind(this); + this.update = this.update.bind(this); + } + + get(): A { return this.value; } + + /** Notify callbacks on change, using _deep_ structural comparison. */ + set(value: A) { + if (!isEqual(value, this.value)) { + this.value = value; + Dome.emit(this.event, value); + } + } + + /** State updater. */ + update(upd: updateAction<A>) { + if (upd === null) + this.reset(); + else { + if (typeof upd === 'function') + this.set(upd(this.value)); + else + this.set(upd); + } + } + + /** Restore default value. */ + reset() { + this.set(this.defaultValue); + } + + /** Callback Emitter. */ + on(callback: (value: A) => void) { + Dome.emitter.on(this.event, callback); + } + + /** Callback Emitter. */ + off(callback: (value: A) => void) { + Dome.emitter.off(this.event, callback); + } + +} + +/** + State with possibly undefined initial value. + */ +export class StateOpt<A extends NonFunction> extends StateDef<undefined | A> { + constructor(defaultValue?: A) { + super(defaultValue); + } +} + +// -------------------------------------------------------------------------- +// --- Settings +// -------------------------------------------------------------------------- + +/** + Generic interface to Window and Global Settings. + To be used with [[useSettings]] with instances of its derived classes, + typically [[WindowSettings]] and [[GlobalSettings]]. You should never have + to implement a Settings class on your own. + + All setting values are identified with + an untyped `dataKey: string`, that can be dynamically modified + for each component. Hence, two components might share both datakeys + and settings. + + When several components share the same setting `dataKey` the behavior will be + different depending on the situation: + - for Window Settings, each component in each window retains its own + setting value, although the last modified value from _any_ of them will be + saved and used for any further initial value; + - for Global Settings, all components synchronize to the last modified value + from any component of any window. + + Type safety is ensured by safe JSON encoders and decoders, however, they + might fail at runtime, causing settings value to be initialized to their + fallback and not to be saved nor synchronized. + This is not harmful but annoying. + + To mitigate this effect, each instance of a Settings class has its + own, private, unique symbol that we call its « role ». A given `dataKey` + shall always be used with the same « role » otherwise it is discarded, + and an error message is logged when in DEVEL mode. + */ +abstract class Settings<A> { + + private static keyRoles = new Map<string, symbol>(); + + private readonly role: symbol; + protected readonly decoder: JSON.Safe<A>; + protected readonly encoder: JSON.Encoder<A>; + + /** + Encoders shall be protected against exception. + Use [[JSON.jTry]] and [[JSON.jCatch]] in case of uncertainty. + Decoders are automatically protected internally to the Settings class. + @param role Debugging name of instance roles (each instance has its unique + role, though) + @param decoder JSON decoder for the setting values + @param encoder JSON encoder for the setting values + @param fallback If provided, used to automatically protect your encoders + against exceptions. + */ + constructor( + role: string, + decoder: JSON.Safe<A>, + encoder: JSON.Encoder<A>, + fallback?: A, + ) { + this.role = Symbol(role); + this.encoder = encoder; + this.decoder = + fallback !== undefined ? JSON.jCatch(decoder, fallback) : decoder; + } + + /** + Returns identity if the data key is only + used with the same setting instance. + Otherwise, returns `undefined`. + */ + validateKey(dataKey?: string): string | undefined { + if (dataKey === undefined) return undefined; + const rq = this.role; + const rk = Settings.keyRoles.get(dataKey); + if (rk === undefined) { + Settings.keyRoles.set(dataKey, rq); + } else { + if (rk !== rq) { + if (DEVEL) console.error( + `[Dome.settings] Key ${dataKey} used with incompatible roles`, rk, rq, + ); + return undefined; + } + } + return dataKey; + } + + /** @internal */ + abstract loadData(key: string): JSON.json; + + /** @internal */ + abstract saveData(key: string, data: JSON.json): void; + + /** @internal */ + abstract event: string; + + /** Returns the current setting value for the provided data key. You shall + only use validated keys otherwise you might fallback to default values. */ + loadValue(dataKey?: string) { + return this.decoder(dataKey ? this.loadData(dataKey) : undefined) + } + + /** Push the new setting value for the provided data key. + You shall only use validated keys otherwise further loads + might fail and fallback to defaults. */ + saveValue(dataKey: string, value: A) { + try { this.saveData(dataKey, this.encoder(value)); } + catch (err) { + if (DEVEL) console.error( + '[Dome.settings] Error while encoding value', + dataKey, value, err, + ); + } + } + +} + +/** + Generic React Hook to be used with any kind of [[Settings]]. + You may share `dataKey` between components, or change it dynamically. + However, a given data key shall always be used for the same Setting instance. + See [[Settings]] documentation for details. + @param S The instance settings to be used. + @param dataKey Identifies which value in the settings to be used. + */ +export function useSettings<A>( + S: Settings<A>, + dataKey?: string, +): [A, (update: A) => void] { + + const theKey = React.useMemo(() => S.validateKey(dataKey), [S, dataKey]); + const [value, setValue] = React.useState<A>(() => S.loadValue(theKey)); + + React.useEffect(() => { + if (theKey) { + const callback = () => setValue(S.loadValue(theKey)); + Dome.on(S.event, callback); + return () => Dome.off(S.event, callback); + } + return undefined; + }); + + const updateValue = React.useCallback((update: A) => { + if (!isEqual(value, update)) { + setValue(update); + if (theKey) S.saveValue(theKey, update); + } + }, [S, theKey]); + + return [value, updateValue]; + +} + +/** Window Settings for non-JSON data. + In most situations, you can use [[WindowSettings]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ +export class WindowSettingsData<A> extends Settings<A> { + + constructor( + role: string, + decoder: JSON.Safe<A>, + encoder: JSON.Encoder<A>, + fallback?: A, + ) { + super(role, decoder, encoder, fallback); + } + + event = 'dome.defaults'; + loadData(key: string) { return Dome.getWindowSetting(key) as JSON.json; } + saveData(key: string, data: JSON.json) { Dome.setWindowSetting(key, data); } + +} + +/** Global Settings for non-JSON data. + In most situations, you can use [[WindowSettings]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ +export class GlobalSettingsData<A> extends Settings<A> { + + constructor( + role: string, + decoder: JSON.Safe<A>, + encoder: JSON.Encoder<A>, + fallback?: A, + ) { + super(role, decoder, encoder, fallback); + } + + event = 'dome.settings'; + loadData(key: string) { return Dome.getGlobalSetting(key) as JSON.json; } + saveData(key: string, data: JSON.json) { Dome.setGlobalSetting(key, data); } + +} + +/** Window Settings. + For non-JSON data, use [[WindowSettingsdata]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ +export class WindowSettings<A extends JSON.json> extends WindowSettingsData<A> { + + constructor(role: string, decoder: JSON.Safe<A>, fallback?: A) { + super(role, decoder, JSON.identity, fallback); + } + +} + +/** Global Settings. + For non-JSON data, use [[WindowSettingsdata]] instead. + You can use a [[JSON.Loose]] decoder for optional values. */ +export class GlobalSettings<A extends JSON.json> extends GlobalSettingsData<A> { + + constructor(role: string, decoder: JSON.Safe<A>, fallback?: A) { + super(role, decoder, JSON.identity, fallback); + } + +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/dome.js b/ivette/src/dome/src/renderer/dome.js index 55f1a81cadb1de48ba7d2d72b9ded74d41ba827b..322a76d8bc3242c7d2631da72db37257d2602649 100644 --- a/ivette/src/dome/src/renderer/dome.js +++ b/ivette/src/dome/src/renderer/dome.js @@ -148,10 +148,6 @@ ipcRenderer.on('dome.ipc.command', (_event,argv,wdir) => { emitter.emit('dome.command',argv,wdir); }); -// -------------------------------------------------------------------------- -// --- Main-Process Communication -// -------------------------------------------------------------------------- - // -------------------------------------------------------------------------- // --- Window Management // -------------------------------------------------------------------------- @@ -395,55 +391,69 @@ export function popupMenu( items, callback ) // --- Settings // -------------------------------------------------------------------------- -var globals = {} ; -var globalPatches = {} ; +var globalSettings = new Map(); +var globalPatches = new Map(); -var settings = {} ; -var settingsPatches = {} ; +var windowSettings = new Map(); +var windowPatches = new Map(); + +const initSetting = + (m, data) => _.forEach(data,(value,key) => m.set(key,value)); // initial values => synchronized event -function syncSettings() { +const syncSettings = () => { const fullSettings = ipcRenderer.sendSync('dome.ipc.settings.sync'); - globals = fullSettings.globals ; - settings = fullSettings.settings ; -} + initSetting( globalSettings, fullSettings.globals ); + initSetting( windowSettings, fullSettings.settings ); +}; const readSetting = ( local, key, defaultValue ) => { - const value = _.get( local ? settings : globals , key ); + const store = local ? windowSettings : globalSettings; + const value = store.get(key); return value === undefined ? defaultValue : value ; }; const writeSetting = ( local, key, value ) => { - if (key) { - const theValue = value===undefined ? null : value ; - const store = local ? settings : globals ; - const patches = local ? settingsPatches : globalPatches ; - _.set( store, key, theValue ); - _.set( patches, key, theValue ); + const store = local ? windowSettings : globalSettings; + const patches = local ? windowPatches : globalPatches; + if (value === undefined) { + store.delete(key); + patches.set(key,null); + } else { + store.set(key,value); + patches.set(key,value); + } + if (local) { + fireSaveSettings(); + } else { emitter.emit('dome.settings'); - if (local) { - if (DEVEL) fireSaveSettings(); - } else { - fireSaveGlobals(); - } + fireSaveGlobals(); + } +}; + +const flushPatches = (m) => { + if (m.size > 0) { + const args = []; + m.forEach((value,key) => { + args.push({ key, value }); + }); + m.clear(); + return args; } + return undefined; }; const fireSaveSettings = _.debounce( () => { - if (!_.isEmpty(settingsPatches)) { - ipcRenderer.send( 'dome.ipc.settings.window', settingsPatches ) ; - settingsPatches = {} ; - } + const args = flushPatches(windowPatches); + args && ipcRenderer.send( 'dome.ipc.settings.window', args ) ; }, 100 ); const fireSaveGlobals = _.debounce( () => { - if (!_.isEmpty(globalPatches)) { - ipcRenderer.send( 'dome.ipc.settings.global', globalPatches ) ; - globalPatches = {} ; - } + const args = flushPatches(globalPatches); + args && ipcRenderer.send( 'dome.ipc.settings.global', args ) ; }, 100 ); @@ -456,25 +466,32 @@ ipcRenderer.on('dome.ipc.closing', (_evt) => { }); /** @event 'dome.settings' - @description Emitted when the settings have been updated. */ + @description Emitted when the global settings have been updated. */ /** @event 'dome.defaults' - @description Emitted when the settings have been reset to default. */ + @description Emitted when the window settings have re-initialized. */ ipcRenderer.on('dome.ipc.settings.defaults',(sender) => { fireSaveSettings.cancel(); fireSaveGlobals.cancel(); - settingsPatches = {}; - globalPatches = {}; - settings = {}; - globals = {}; - emitter.emit('dome.defaults'); + windowPatches.clear(); + globalPatches.clear(); + windowSettings.clear(); + globalSettings.clear(); emitter.emit('dome.settings'); + emitter.emit('dome.defaults'); }); ipcRenderer.on('dome.ipc.settings.update',(sender,patches) => { - // Don't cancel local updates - _.merge( globals , patches , globalPatches ); + patches.forEach(({ key, value }) => { + // Don't cancel local updates + if (!globalPatches.has(key)) { + if (value === null) + globalSettings.delete(key); + else + globalSettings.set(key,value); + } + }); emitter.emit('dome.settings'); }); @@ -493,7 +510,7 @@ export function getWindowSetting( key, defaultValue ) { } /** @summary Set value into local window (persistent) settings. - @param {string} key to store the data + @param {string} [key] to store the data @param {any} value associated value or object @description This settings are local to the current window, but persistently @@ -501,7 +518,7 @@ export function getWindowSetting( key, defaultValue ) { For global application settings, use `setGlobal()` instead. */ export function setWindowSetting( key , value ) { - writeSetting( true, key, value ); + key && writeSetting( true, key, value ); } /** @@ -524,7 +541,8 @@ export function getGlobalSetting( key, defaultValue ) { @description These settings are global to the current window, but persistently saved in the user's home directory. Updated values are broadcasted - in batch to all other windows, which in turn receive a `'dome.settings'` + in batch to all other windows, + which in turn receive a `'dome.settings'` event for synchronizing.<br/> For local window settings, use `set()` instead. */ @@ -684,25 +702,21 @@ export function useCommand() { function useSettings( local, settings, defaultValue ) { - const [ value, setValue ] = React.useState(() => readSetting( local, settings, defaultValue )); + const [ value, setValue ] = + React.useState(() => readSetting( local, settings, defaultValue )); React.useEffect(() => { - if (settings) { - let callback = () => { - let v = readSetting( local, settings , defaultValue ); - setValue(v); - }; - emitter.on('dome.settings',callback); - return () => emitter.off( 'dome.settings', callback ); - } else { - let callback = () => setValue(defaultValue); - emitter.on('dome.defaults',callback); - return () => emitter.off( 'dome.defaults', callback ); - } + let callback = () => { + let v = readSetting( local, settings , defaultValue ); + setValue(v); + }; + const event = local ? 'dome.defaults' : 'dome.settings' ; + emitter.on(event,callback); + return () => emitter.off(event, callback); }); const doUpdate = (upd) => { const theValue = typeof(upd)==='function' ? upd(value) : upd ; if (settings) writeSetting( local, settings, theValue ); - else setValue(theValue); + if (local) setValue(theValue); }; return [ value, doUpdate ]; } @@ -715,8 +729,7 @@ function useSettings( local, settings, defaultValue ) @description Similar to `React.useState()` with persistent _window_ settings. When the settings key is undefined, it simply uses a local React state. - Also responds to `'dome.settings'` to update the state and `'dome.defaults'` - to restore the default value. + Also responds to `'dome.defaults'`. The `setValue` callback accepts either a value, or a function to be applied on current value. @@ -750,8 +763,7 @@ export function useSwitch( settings, defaultValue=false ) @description Similar to `React.useState()` with persistent _global_ settings. When the settings key is undefined, it simply uses a local React state. - Also responds to `'dome.settings'` to update the state and `'dome.defaults'` - to restore the default value. + Also responds to `'dome.settings'` to update the state. The `setValue` callback accepts either a value, or a function to be applied on current value. diff --git a/ivette/src/dome/src/renderer/frame/sidebars.js b/ivette/src/dome/src/renderer/frame/sidebars.js index 08ed6ae624eea139d9d54fac1198d05a1c90a685..6fcd8e42a50b7fca2f0dff2261ebd09b4daf413b 100644 --- a/ivette/src/dome/src/renderer/frame/sidebars.js +++ b/ivette/src/dome/src/renderer/frame/sidebars.js @@ -108,7 +108,7 @@ export function Section(props) { const context = React.useContext( SideBarContext ); const [ state=true, setState ] = Dome.useState( - makeSettings(context,props), + makeSettings(context.settings,props), props.defaultUnfold ); const { enabled=true, disabled=false, unfold, children } = props ; diff --git a/ivette/src/dome/src/renderer/table/views.tsx b/ivette/src/dome/src/renderer/table/views.tsx index 953cdbdf5901e39f3b9ae3eff6de5a7affa1f1b8..b485a7e8a5ccb7d062c7bd6e61eac70aeba48002 100644 --- a/ivette/src/dome/src/renderer/table/views.tsx +++ b/ivette/src/dome/src/renderer/table/views.tsx @@ -212,7 +212,7 @@ function makeDataGetter( if (rowData !== undefined) return getter(rowData, dataKey); } catch (err) { console.error( - '[Dome.table] custom getter error', + '[Dome.table] Custom getter error', 'rowData:', rowData, 'dataKey:', dataKey, err, @@ -240,7 +240,7 @@ function makeDataRenderer( return contents; } catch (err) { console.error( - '[Dome.table] custom renderer error', + '[Dome.table] Custom renderer error', 'dataKey:', props.dataKey, 'cellData:', cellData, err, diff --git a/ivette/src/dome/src/renderer/text/buffers.js b/ivette/src/dome/src/renderer/text/buffers.js index 2546e8f5b7cc81d1a159c4a2d218c811fc028625..444970d2c104736ea9ac09970ea030638a836dfa 100644 --- a/ivette/src/dome/src/renderer/text/buffers.js +++ b/ivette/src/dome/src/renderer/text/buffers.js @@ -564,7 +564,7 @@ is blocked. } else if (typeof text === 'string') { this.append(text); } else if (text !== null) { - console.error('[Dome.buffers] unexpected text',text); + console.error('[Dome.buffers] Unexpected text', text); } } diff --git a/ivette/src/dome/template/makefile.packages b/ivette/src/dome/template/makefile.packages index 05b8cb03aece9be54813a53d3952d9e804943839..34a494d77320ff171a14873d6eb15b4456e9bbc0 100644 --- a/ivette/src/dome/template/makefile.packages +++ b/ivette/src/dome/template/makefile.packages @@ -25,6 +25,7 @@ DOME_APP_PACKAGES= \ lodash \ react-virtualized \ react-draggable \ + react-fast-compare \ codemirror # -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/LabViews.tsx b/ivette/src/frama-c/LabViews.tsx index d41a4bc67b37746d9f5fe3c1b9b0e1a0a9928a24..52bc3151cac0951f6f9642dee1e7ceb83b05dc6b 100644 --- a/ivette/src/frama-c/LabViews.tsx +++ b/ivette/src/frama-c/LabViews.tsx @@ -355,7 +355,6 @@ function CustomViews({ settings, shape, setShape, views: libViews }: any) { const [edited, setEdited]: any = React.useState(); const triggerDefault = React.useRef(); const { current, shapes = {} } = local; - const theViews: any = {}; _.forEach(libViews, (view) => { diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx index 01d73e3cb1486b920d9fbd9e812189eac70cbe75..699fa98dc8a2018dc87afac4848253179e67aaad 100644 --- a/ivette/src/renderer/Controller.tsx +++ b/ivette/src/renderer/Controller.tsx @@ -169,17 +169,10 @@ const editor = new RichTextBuffer(); const RenderConsole = () => { const scratch = React.useRef([] as string[]); const [cursor, setCursor] = React.useState(-1); - const [H0, setH0] = Dome.useState('Controller.history', []); + const [history, setHistory] = Dome.useState('Controller.history', []); const [isEmpty, setEmpty] = React.useState(true); const [noTrash, setNoTrash] = React.useState(true); - // Cope with merge settings that keeps previous array entries (BUG in DOME) - const history = Array.isArray(H0) ? H0.filter((h) => h !== '') : []; - const setHistory = (hs: string[]) => { - const n = hs.length; - setH0(n < 50 ? hs.concat(Array(50 - n).fill('')) : hs); - }; - Dome.useEmitter(editor, 'change', () => { const cmd = editor.getValue().trim(); setEmpty(cmd === '');