diff --git a/ivette/api/server_tsc.ml b/ivette/api/server_tsc.ml index 8f5eb6e829b7992c706487f59d1b23b226f80421..62324078a05818e35a5c0802eaa4e2c07006d566 100644 --- a/ivette/api/server_tsc.ml +++ b/ivette/api/server_tsc.ml @@ -76,7 +76,7 @@ let makeJtype ?self ~names = | Jstring | Jalpha -> Format.pp_print_string fmt "string" | Jkey kd -> Format.fprintf fmt "Json.key<'#%s'>" kd | Jindex kd -> Format.fprintf fmt "Json.index<'#%s'>" kd - | Jdict(kd,js) -> Format.fprintf fmt "Json.Dict<'#%s',%a>" kd pp js + | Jdict(kd,js) -> Format.fprintf fmt "Json.Dictionary<'#%s',%a>" kd pp js | Jdata id | Jenum id -> pp_ident fmt id | Joption js -> Format.fprintf fmt "%a |@ undefined" pp js | Jtuple js -> diff --git a/ivette/src/dome/doc/guides/application.md b/ivette/src/dome/doc/guides/application.md index 8abb0b0ebdce327a8a1cf2d20d71f4b28bc69a02..ae191b062b31701c1d63c0ba49e4ad50526d7761 100644 --- a/ivette/src/dome/doc/guides/application.md +++ b/ivette/src/dome/doc/guides/application.md @@ -85,15 +85,10 @@ your data flow. - **Global States** are necessary to implement the unidirectional data-flow. These data are stored in the renderer process, but outside of the view hierarchy of **React** components actually mounted in the DOM. Hence, it remains consistent whatever - the evolution of the view. See `Dome.State` class and the associated custom **React** hooks + the evolution of the view. See `dome/state` module and the associated custom **React** hooks to implement global states. You may also use global JavaScript variables and emit events on your own. -- **Local States** are necessary to maintain local states associated - to views. We strongly encourage the use of the `Dome.useState()` hook for this - purpose, since it generalizes `React.useState()` with persistent window settings - (see below). - - **View Updates** to make your views listening for updates of the various data sources, we encourage to use the **React** hooks we provide, since they transparently make your components to re-render when required. However, @@ -110,12 +105,12 @@ your data flow. **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 - `Dome.setWindowSetting()` and `Dome.getWindowSetting()`, or the **React** hook - `Dome.useWindowSetting()`. + `Settings.setWindowSetting()` and `Settings.getWindowSetting()`, or the **React** hook + `Settings.useWindowSetting()`. See also helpers `Dome.useXxxSettings()`. - **Global Settings** are stored in the user's home directory and automatically saved and load with your application; they are typically modified _via_ the Settings Window, which is accessible from the application menubar. In **Dome**, - you access these data by using `Dome.setGlobalSetting()` and - `Dome.getGlobalSetting()`, or the **React** hook `Dome.useGlobalSetting()`. - Settings must be JSON serializable JavaScript values. + you can shall define a global settings by creating an instance of + `Settings.GlobalSettings` class and use it with + the `Settings.useGlobalSettings()` hook. diff --git a/ivette/src/dome/src/renderer/data/json.ts b/ivette/src/dome/src/renderer/data/json.ts index 2e2ebe87b2de5fc4ad4c8ced72592c3aa2ce7a04..1f5b2163ed92e12a322c8d950f4805abd2f73a68 100644 --- a/ivette/src/dome/src/renderer/data/json.ts +++ b/ivette/src/dome/src/renderer/data/json.ts @@ -429,32 +429,55 @@ export function jIndex<K>(kd: K): Loose<index<K>> { return (js: json) => typeof js === 'number' ? forge(kd, js) : undefined; } +/** Dictionaries with « untyped » keys. */ +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 jDict<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 key of Object.keys(js)) { + const fd = js[key]; + if (fd !== undefined) { + const fv = fn(fd); + if (fv !== undefined) buffer[key] = fv; + } + } + } + return buffer; + }; +} + /** Dictionaries with « typed » keys. */ -export type dict<K, A> = phantom<K, { [key: string]: A }> +export type dictionary<K, A> = phantom<K, { [key: string]: A }> /** Lookup into dictionary. Better than a direct access to `d[k]` for undefined values. */ -export function lookup<K, A>(d: dict<K, A>, k: key<K>): A | undefined { +export function lookup<K, A>(d: dictionary<K, A>, k: key<K>): A | undefined { return d[k]; } /** Empty dictionary. */ -export function empty<K, A>(kd: K): dict<K, A> { +export function empty<K, A>(kd: K): dictionary<K, A> { return forge(kd, {} as any); } /** Dictionary extension. */ -export function index<K, A>(d: dict<K, A>, key: key<K>, value: A) { +export function index<K, A>(d: dictionary<K, A>, key: key<K>, value: A) { d[key] = value; } /** - Decode a JSON dictionary, discarding all inconsistent entries. + Decode a JSON dictionary with typed keys, discarding all inconsistent entries. If the JSON contains no valid entry, still returns `{}`. */ -export function jDictionary<K, A>(kd: K, fn: Loose<A>): Safe<dict<K, A>> { +export function jDictionary<K, A>(kd: K, fn: Loose<A>): Safe<dictionary<K, A>> { return (js: json) => { - const buffer: dict<K, A> = empty(kd); + const buffer: dictionary<K, A> = empty(kd); if (js !== null && typeof js === 'object' && !Array.isArray(js)) { for (var key of Object.keys(js)) { const fd = js[key]; @@ -472,8 +495,8 @@ export function jDictionary<K, A>(kd: K, fn: Loose<A>): Safe<dict<K, A>> { Encode a dictionary into JSON, discarding all inconsistent entries. If the dictionary contains no valid entry, still returns `{}`. */ -export function eDictionary<K, A>(fn: Encoder<A>): Encoder<dict<K, A>> { - return (d: dict<K, A>) => { +export function eDictionary<K, A>(fn: Encoder<A>): Encoder<dictionary<K, A>> { + return (d: dictionary<K, A>) => { const js: json = {}; for (var k of Object.keys(d)) { const fv = d[k]; diff --git a/ivette/src/dome/src/renderer/dome.js b/ivette/src/dome/src/renderer/dome.js deleted file mode 100644 index face33d62a13c994b370e894f179d2c9c5a23f16..0000000000000000000000000000000000000000 --- a/ivette/src/dome/src/renderer/dome.js +++ /dev/null @@ -1,827 +0,0 @@ -/** - @packageDocumentation - @module dome(renderer) - @description - - ## Dome Application (Renderer Process) - - This modules manages your main application window - and its interaction with the main process. - - Example: - - ```typescript - // File 'src/renderer/index.js': - import Application from './Application.js' ; - Dome.setContent( Application ); - ``` - */ - -import _ from 'lodash' ; -import React from 'react'; -import ReactDOM from 'react-dom'; -import { AppContainer } from 'react-hot-loader' ; -import { remote , ipcRenderer } from 'electron'; -import { EventEmitter } from 'events' ; -import SYS , * as System from 'dome/system' ; -import './style.css' ; - -// -------------------------------------------------------------------------- -// --- Context -// -------------------------------------------------------------------------- - -// main window focus -var focus = true ; - -function setContextAppNode() -{ - const node = document.getElementById('app'); - if (node) { - node.className = - 'dome-container dome-platform-' + System.platform + - ( focus ? ' dome-window-active' : ' dome-window-inactive' ) ; - } - return node; -} - -// -------------------------------------------------------------------------- -// --- Helpers -// -------------------------------------------------------------------------- - -/** @summary Development mode flag. - @description - Configured to be `'true'` when in development mode -*/ -export const DEVEL = System.DEVEL ; - -/** @summary System platform. - @description - Same as `platform` from `dome/system` */ -export const platform = System.platform ; - -// -------------------------------------------------------------------------- -// --- Application Emitter -// -------------------------------------------------------------------------- - -/** @summary Application Emitter. - @description - Can be used as a basic _Flux_ dispatcher. */ -export const emitter = new EventEmitter(); - -/** Same as `emitter.on` */ -export function on(evt,job) { emitter.on(evt,job); } - -/** Same as `emitter.off` */ -export function off(evt,job) { emitter.off(evt,job); } - -/** Same as `emitter.emit` */ -export function emit(evt,...args) { emitter.emit(evt,...args); } - -{ - emitter.setMaxListeners(250); -} - -// -------------------------------------------------------------------------- -// --- Application Events -// -------------------------------------------------------------------------- - -/** @event 'dome.update' - @description - Convenient pre-defined events for triggering a global re-render. - See also [Dome.onUpdate](#.onUpdate), [Dome.update](#.update) methods and - the [Dome.useUpdate](#.useUpdate) hook. -*/ - -/** @event 'dome.reload' - @description - Triggered when the application has been loaded or re-loaded - See also [Dome.onReload](#.onReload). -*/ - -/** @event 'dome.command' - @param {Array.<string>} argv - command line arguments - @param {string} wdir - working directory - @description - Triggered when the command line argument has been received, and when - the application is re-loaded (in development mode). - - See also [Dome.onCommand](#.onCommand). -*/ - -/** - @summary Emits the `dome.update` event. -*/ -export function update() { emitter.emit('dome.update'); } - -/** - @summary Update event handler. - @param {function} cb - invoked on update events. - @description - Register a callback on [dome.update](#~event:'dome.update') event. -*/ -export function onUpdate(job) { emitter.on('dome.update',job); } - -/** - @summary Update event handler. - @param {function} cb - invoked on reload events. - @description - Register a callback on [dome.reload](#~event:'dome.reload') event. -*/ -export function onReload(job) { emitter.on('dome.reload',job); } - -/** @summary Command-line event handler. - @param {function} cb - invoked with `cb(argv,wdir)` - @description -Register a callback on [dome.command](#~event:'dome.reload') event, -emitted by the `Main` process when the application instance is launched. - -See also: - - [[useCommand]] - - `System.getArguments` - - `System.getWorkingDir` -*/ -export function onCommand(job) { emitter.on('dome.command',job); } - -ipcRenderer.on('dome.ipc.reload',() => emitter.emit('dome.reload')); -ipcRenderer.on('dome.ipc.command', (_event,argv,wdir) => { - SYS.SET_COMMAND(argv,wdir); - emitter.emit('dome.command',argv,wdir); -}); - -// -------------------------------------------------------------------------- -// --- Window Management -// -------------------------------------------------------------------------- - -export function isApplicationWindow() -{ - return process.argv.includes( SYS.WINDOW_APPLICATION_ARGV ); -} - -export function isPreferencesWindow() -{ - return process.argv.includes( SYS.WINDOW_PREFERENCES_ARGV ); -} - -// -------------------------------------------------------------------------- -// --- Window Title -// -------------------------------------------------------------------------- - -export function setModified( modified ) -{ - ipcRenderer.send('dome.ipc.window.modified',modified); -} - -export function setTitle( title ) -{ - ipcRenderer.send('dome.ipc.window.title',title); -} - -// -------------------------------------------------------------------------- -// --- Main Content -// -------------------------------------------------------------------------- - -/** - @summary Defines the user's main window content. - @param {React.Component} Component - to be rendered in the main window - @description - Binds the component to the main window. - - <strong>Notes:</strong> a `<Component/>` instance is generated and rendered in the `#app` - window element. Its class name is set to `dome-platform-<platform>` with - the `<platform>` set to the `Dome.platform` value. This class name can be used - as a CSS selector for platform-dependent styling. -*/ -export function setApplicationWindow( Component ) -{ - if (isApplicationWindow()) { - syncSettings(); - const appNode = setContextAppNode(); - ReactDOM.render( <AppContainer><Component/></AppContainer> , appNode ); - } -} - -// -------------------------------------------------------------------------- -// --- Settings Window -// -------------------------------------------------------------------------- - -/** - @summary Defines the user's preferences window content. - @param {React.Component} Component - to be rendered in the settings window - @description - Binds the component to the settings window. - - <strong>Notes:</strong> a `<Component/>` instance is generated and rendered in the `#app` - window element. Its class name is set to `dome-platform-<platform>` with - the `<platform>` set to the `Dome.platform` value. This class name can be used - as a CSS selector for platform-dependent styling. -*/ -export function setPreferencesWindow( Component ) -{ - if (isPreferencesWindow()) { - syncSettings(); - const appNode = setContextAppNode(); - ReactDOM.render( <AppContainer><Component/></AppContainer> , appNode ); - } -} - -// -------------------------------------------------------------------------- -// --- MenuBar Management -// -------------------------------------------------------------------------- - -const customItemCallbacks = {} ; - -/** - @summary Create a new custom menu in the menu bar. - @param {string} label - the menu title (shall be unique) - @description - This function can be triggered at any time, and will eventually trigger - an update of the whole application menubar. - - It is also possible to call this function from the main process. -*/ -export function addMenu( label ) { ipcRenderer.send( 'dome.ipc.menu.addmenu' , label ); } - -/** - @summary Insert a new custom item in a menu. - @param {object} spec - the menu-item specification - @description -The menu-item shall be specified by using the following fields: - - `menu` (`string`, _required_) : the label of the menu to insert the item in; - can be a custom menu, or one of the predefined `'File'`, `'Edit'` or `'View'` menus. - - `id` (`string|number`, _required_) : the item identifier; - shall be unique among the entire menu-bar. - - `type` (`string`, _optional_) : one of `'normal'`, `'separator'`, `'checkbox'` or `'radio'`. - - `label` (`string`, _optional_) : the item label. - - `visible` (`boolean`, _optional_, default is `true`). - - `enabled` (`boolean`, _optional_, default is `true`). - - `checked` (`boolean`, _optional_, for `type:'checkbox'` and `type:'radio'` only, default is `false`). - - `key` (`string`, _optional_) : a keyboard shortcut for menu-item. - - `onClick` (`function`, _optional_) : an optional callback. - -These options (except `menu` and `id`) can be modified later on by using the [setMenuItem](#.setMenuItem) function. - -When clicked, the menu-item will also trigger a `'dome.menu.clicked'` event on the entire application (both process) -with the corresponding `id`. - -Key short cuts shall be specified with the following codes: - - `"Cmd+<Key>"` for command (MacOS) or control (Linux) key - - `"Alt+<Key>"` for command+option (MacOS) or alt (Linux) key - - `"Meta+<Key>"` for command+shift (MacOS) or control+alt (Linux) key - -Alternatively, more precise keybord shortcuts can be specified with the `'accelerator'` option, -which follows the same encoding that menu-item specifications from Electron. - -The `addMenu` function can be triggered at any time, and will eventually trigger -an update of the whole application menubar. -It is also possible to call this function from the main process. - -*/ -export function addMenuItem( spec ) -{ - if (!spec.id && spec.type !== 'separator') { - console.error('[Dome] Missing menu-item identifier',spec); - return; - } - const { onClick , ...options } = spec ; - if ( onClick ) customItemCallbacks[ spec.id ] = onClick ; - ipcRenderer.send( 'dome.ipc.menu.addmenuitem' , options ); -} - -/** - @summary Update properties of an existing menu-item. - @param {object} options - the menu-item specification to update - @description - Options must follow the specification of the [addMenuItem](#.addMenuItem) function. - Option `id` must specify the identifier of the menu item to update. - The menu and item positions can _not_ be modified. - If an `onClick` callback is specified, it will _replace_ the previous one. - You shall specify `null` to remove the previously registered callback - (`undefined` callback is ignored). - - This function can be triggered at any time, and will possibly trigger - an update of the whole application menubar if the properties - can not be changed dynamically in Electron. - - It is also possible to call this function from the main process. - When specified, the item callback is only invoked in the process which - specify it. To register callbacks in other process, - you shall listen to the `'dome.menu.clicked'` event. - */ -export function setMenuItem( options ) { - if (!options.id) { - console.error('[Dome] Missing menu-item identifier',options); - return; - } - const { onClick , ...updates } = options ; - if (onClick !== undefined) { - if (onClick) customItemCallbacks[options.id] = onClick ; - else delete customItemCallbacks[options.id] ; - } - ipcRenderer.send( 'dome.ipc.menu.setmenuitem', updates ); -} - -/** @event 'dome.menu.clicked' - @description Emitted with the clicked menu-item identifier */ - -ipcRenderer.on('dome.ipc.menu.clicked',(id) => { - const callback = customItemCallbacks[id] ; - callback && callback(); -}); - -// -------------------------------------------------------------------------- -// --- Context Menus -// -------------------------------------------------------------------------- - -/** - @summary Popup a contextual menu. - @param {item[]} items - the array of menu items - @param {function} [callback] - an optional callback - @description -Each menu item is specified by an object with the following fields: - - `id` (`string|number`, _optional_) : the item identifier. - - `label` (`string`, _optional_) : the item label. - - `enabled` (`boolean`, _optional_, default is `true`). - - `display` (`boolean`, _optional_, default is `true`). - - `checked` (`boolean`, _optional_, default is `undefined`). - - `onClick` (`function`, _optional_) : callback on item selection. - -Items can be separated by inserting a `'separator'` constant string -in the array. Item identifier and label default to each others. Alternatively, -an item can be specified by a single string that will be used for both -its label and identifier. Undefined or null items are allowed (and skipped). - -The menu is displayed at the current mouse location. -The callback is called with the selected item identifier or label. -If the menu popup is canceled by the user, the callback is called with `undefined`. - -@example -let myPopup = (_evt) => Dome.popupMenu([ …items… ],(id) => … ); -<div onRightClick={myPopup}>...</div> - -*/ -export function popupMenu( items, callback ) -{ - const { Menu , MenuItem } = remote ; - const menu = new Menu(); - var selected = undefined ; - var kid = 0 ; - items.forEach((item) => { - if (item === 'separator') - menu.append(new MenuItem({ type:'separator' })); - else if (item) - { - const { display=true, enabled, checked } = item ; - if (display) { - const label = item.label || '#'+(++kid) ; - const id = item.id || label ; - const click = () => { - selected = id ; - item.onClick && item.onClick(); - }; - const type = checked !== undefined ? 'checkbox' : 'normal' ; - menu.append(new MenuItem({ label, enabled, type, checked, click })); - } - } - }); - const job = callback ? () => callback( selected ) : undefined ; - menu.popup({window: remote.getCurrentWindow(), callback:job }); -} - -// -------------------------------------------------------------------------- -// --- Closing -// -------------------------------------------------------------------------- - -ipcRenderer.on('dome.ipc.closing', System.doExit); - -// -------------------------------------------------------------------------- -// --- Focus Management -// -------------------------------------------------------------------------- - -/** Current focus state of the main window. */ -export function isFocused() { return focus; } - -/** - @event 'dome.focus' - @param {boolean} state - updated focus state - @description Emitted when the application gain or loses focus. -*/ -ipcRenderer.on('dome.ipc.focus',(sender,value) => { - focus = value; - setContextAppNode(); - emitter.emit('dome.focus',value); -}); - -// -------------------------------------------------------------------------- -// --- Web Navigation -// -------------------------------------------------------------------------- - -/** - @event 'dome.href' - @param {string} href - internal `<a href=...>` target - @description - Emitted when the user clicks on a local `<a href=...>`. - URL with an `http://` protocole are opened externally - by the user's default browser. -*/ -ipcRenderer.on('dome.ipc.href',(href) => emitter.emit('dome.href',href)); - -// -------------------------------------------------------------------------- -// --- Function Component -// -------------------------------------------------------------------------- - -/** - @summary Inlined Function React Component. - @property {function} children - render function as children - @description - Allows to define an inlined functional component inside JSX. - The children function _can_ use hooks. - -@example -<Render> - {() => { - let [ state, setState ] = React.useState(); - … - return (<div>…</div>); - }} -</Render> -*/ -export const Render = ({children}) => { - return children(); -}; - -// -------------------------------------------------------------------------- -// --- React Hooks -// -------------------------------------------------------------------------- - -/** - @summary Hook to re-render on demand (Custom React Hook). - @return {function} to trigger re-rendering - @description - Returns a callback to trigger a render on demand. -*/ -export function useForceUpdate() -{ - const [tac,onTic] = React.useState(); - return () => onTic(!tac); -} - -/** - @summary Hook to re-render on Dome events (Custom React Hook). - @param {string} [event,...] - event names (default: `'dome.update'`) - @description - Returns nothing. -*/ -export function useUpdate(...evts) -{ - const update = useForceUpdate(); - React.useEffect(() => { - const trigger = () => setImmediate(update); - if (evts.length == 0) evts.push('dome.update'); - evts.forEach((evt) => emitter.on(evt,trigger)); - return () => evts.forEach((evt) => emitter.off(evt,trigger)); - }); -} - -/** - @summary Hook to register callbacks to Dome events (Custom React Hook). - @param {string} event - Event to register on - @param {function} callback - The callback to register - @description - Register the callback on event until the component is unmount. - Do not force the component to re-render (unless the callback does).<br/> - Returns nothing. -*/ -export function useEvent(evt,callback) -{ - React.useEffect(() => { - emitter.on(evt,callback); - return () => emitter.off(evt,callback); - }); -} - -/** - @summary Hook to register callbacks to events (Custom React Hook). - @param {EventEmitter} emitter - event emitter - @param {string} event - Event to register on - @param {function} callback - The callback to register - @description - Register the callback on event until the component is unmount. - Do not force the component to re-render (unless the callback does).<br/> - Returns nothing. -*/ -export function useEmitter(emitter,evt,callback) -{ - React.useEffect(() => { - emitter.on(evt,callback); - return () => emitter.off(evt,callback); - }); -} - -const NULL = {}; // Dummy initial value - -// -------------------------------------------------------------------------- -// --- Commands Hooks -// -------------------------------------------------------------------------- - -/** - @summary Hook for command-line interface (Custom React Hook). - @return {array} `[argv,wdir]` command-line arguments and working directory - @description - Returns the command-line arguments and working directory for the application - instance running in the window. Automatically updated on `dome.command` events. - - See also [[onCommand]] event handler. -*/ -export function useCommand() { - useUpdate('dome.command'); - const wdir = System.getWorkingDir(); - const argv = System.getArguments(); - return [ argv , wdir ]; -} - -// -------------------------------------------------------------------------- -// --- Settings Hooks -// -------------------------------------------------------------------------- - -function useSettings( local, settings, defaultValue ) -{ - const [ value, setValue ] = - React.useState(() => readSetting( local, settings, defaultValue )); - React.useEffect(() => { - 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 ); - if (local) setValue(theValue); - }; - return [ value, doUpdate ]; -} - -/** - @summary Local state with optional window settings (Custom React Hook). - @param {string} [settings] - optional window settings to backup the value - @param {any} [defaultValue] - the initial (and default) value - @return {array} `[value,setValue]` of the local state - @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.defaults'`. - - The `setValue` callback accepts either a value, or a function to be applied - on current value. -*/ -export function useState( settings, defaultValue ) -{ - return useSettings( true, settings, defaultValue ); -} - -/** - @summary Local boolean state with optional window settings (Custom React Hook). - @param {string} [settings] - optional window settings to backup the value - @param {boolean} [defaultValue] - the initial value (default is `false`) - @return {array} `[value,flipValue]` for the local state - @description - Same as [useState](#.useState) with a boolean value that can be set or flipped: - - `flipValue()` change the value to its opposite; - - `flipValue(v)` change the value to `v`. -*/ -export function useSwitch( settings, defaultValue=false ) -{ - const [ value, update ] = useSettings( true, settings, defaultValue ); - return [ value, v => update(v===undefined ? !value : v) ]; -} - -/** - @summary Local state with global settings (Custom React Hook). - @param {string} settings - global settings for storing the value - @param {any} [defaultValue] - the initial and default value - @return {array} `[value,setValue]` of the local state - @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. - - The `setValue` callback accepts either a value, or a function to be applied - on current value. -*/ -export function useGlobalSetting( settings, defaultValue ) -{ - return useSettings( false, settings, defaultValue ); -} - -// -------------------------------------------------------------------------- -// --- Global States -// -------------------------------------------------------------------------- - -/** @event 'dome.state.update' - @description - Notify updates within a State object. -*/ - -const STATE_UPDATE = 'dome.state.update' ; - -/** - @summary Global state object. - @property {object} state - the current state properties - @property {object} defaults - the default state properties - @description - -You may use this class as convenient way to implement global -state for your Dome application. Simply create a state `s` with `new State(defaults)` -and use `s.setState()`, `s.getState()` or `s.state` property, and `s.useState()` -custom hooks. - -A state is also an event emitter that you can use to fire events, and you can use -the React custom hooks `s.useUpdate()` and `s.useEvent()`. - -All above methods are bound to `this` by the constructor. - -*/ -export class State extends EventEmitter -{ - - constructor(props) { - super(); - // Makes this field private - this.defaults = props ; - this.state = Object.assign( {}, props ); - this.update = this.update.bind(this); - this.getState = this.getState.bind(this); - this.setState = this.setState.bind(this); - this.clearState = this.clearState.bind(this); - this.replaceState = this.replaceState.bind(this); - this.useState = this.useState.bind(this); - this.useEvent = this.useEvent.bind(this); - this.useUpdate = this.useUpdate.bind(this); - } - - /** Emits the `dome.state.update` event */ - update() { this.emit('dome.state.update'); } - - /** Returns the state property. */ - getState() { return this.state; } - - /** @summary Update the state with (some) properties. - @param {object} props - the properties to be updated - @description - Update the state with `Object.assign`, like `setState()` on React components. - Also fire the `'dome.update'` property on the object. */ - setState(props) { - Object.assign( this.state, props ); - this.update(); - } - - /** @summary Replace (all) state properties. - @description - Replace the entire store with the new properties. - Also fire the `'dome.update'` property on the object. */ - replaceState(props) { - this.state = Object.assign( {}, props ); - this.update(); - } - - /** @summary Reset (all) state properties. - @description - Restore the entire store with the default properties. - Also fire the `'dome.state.update'` property on the object. */ - clearState() { - this.state = Object.assign( {}, this.defaults ); - this.update(); - } - - /** @summary Hook to use the state (custom React Hook). - @return {array} `[state,setState]` with current object properties and - function to update them. */ - useState() { - let forceUpdate = useForceUpdate(); - useEmitter( this, 'dome.state.update', forceUpdate ); - return [ this.state , this.setState ]; - } - - /** @summary Hook to re-render your component on State events. - @param {string} [event] - the event to listen to (defaults to `'dome.update'`) - */ - useUpdate(evt = 'dome.state.update') { - let forceUpdate = useForceUpdate(); - useEmitter( this, evt, forceUpdate ); - } - - /** @summary Hook to trigger callbacks on State events. - @param {string} event - the event to listen to - @param {function} callback - the callback triggered on event - */ - useEvent(evt,callback) { - useEmitter( this, evt, callback ); - } - -} - -// -------------------------------------------------------------------------- -// --- Timer Hooks -// -------------------------------------------------------------------------- - -// Collection of { pending, timer, period, time, event } indexed by period -const clocks = {}; - -const CLOCKEVENT = (period) => 'dome.clock.' + period ; - -const TIC_CLOCK = (clk) => () => { - if (0 < clk.pending) { - clk.time += clk.period ; - emitter.emit(clk.event,clk.time); - } else { - clearInterval(clk.timer); - delete clocks[clk.period]; - } -}; - -const INC_CLOCK = (period) => { - let clk = clocks[period] ; - if (!clk) { - let event = CLOCKEVENT(period); - let time = (new Date()).getTime(); - clk = { pending: 0, time, period, event }; - clocks[period] = clk ; - let tic = TIC_CLOCK(clk); - clk.timer = setInterval(tic,period); - } - clk.pending++; -}; - -const DEC_CLOCK = (period) => { - let clk = clocks[period] ; - if (clk) { - clk.pending--; - } -}; - -/** - @summary Synchronized start & stop timer (Custom React Hook). - @param {number} period - timer interval, in milliseconds (ms) - @param {boolean} [initStart] - whether to initially start the timer (default is `false`) - @return {timer} Timer object - @description - Create a local timer, synchronized on a global clock, that can be started - and stopped on demand during the life cycle of the component. - - Each timer has its individual start & stop state. However, - all timers with the same period _are_ synchronized with each others. - - The timer object has the following properties and methods: - - `timer.start()` starts the timer, - - `timer.stop()` starts the timer, - - `timer.time` is the time stamp of the last clock (see below) - - It is safe to call `start()` and `stop()` whether the timer is running or not. - When `timer.time` is `-1`, it means the timer is stopped. - When `timer.time` is `0` it means the timer has just been started and no tic has - been received yet. The time stamp is in milliseconds; it is shared among all - timers synchronized on the same period and roughly equal to the `Date.getTime()` - of the associated clock. - - */ - -export function useClock(period,initStart) -{ - const [time,setTime] = React.useState(initStart ? 0 : -1); - const running = 0 <= time ; - React.useEffect(() => { - if (running) { - INC_CLOCK(period); - const event = CLOCKEVENT(period); - emitter.on(event,setTime); - return () => { - DEC_CLOCK(period); - emitter.off(event,setTime); - }; - } else - return undefined ; - }); - return { - time, - start: () => { if (!running) setTime(0); }, - stop: () => { if (running) setTime(-1); } - }; -} - -// -------------------------------------------------------------------------- -// --- Pretty Printing (Browser Console) -// -------------------------------------------------------------------------- - -export class PP { - constructor(moduleName) { - this.moduleName = moduleName; - } - log(...args) { console.log(`[${this.moduleName}]`,...args); } - warn(...args) { console.warn(`[${this.moduleName}]`,...args); } - error(...args) { console.error(`[${this.moduleName}]`,...args); } -} - -// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/dome.ts b/ivette/src/dome/src/renderer/dome.ts new file mode 100644 index 0000000000000000000000000000000000000000..873367b4b83e9842581442305af84f8d4e72b4d9 --- /dev/null +++ b/ivette/src/dome/src/renderer/dome.ts @@ -0,0 +1,659 @@ +/** + @packageDocumentation + @module dome(renderer) + @description + + ## Dome Application (Renderer Process) + + This modules manages your main application window + and its interaction with the main process. + + Example: + + ```typescript + // File 'src/renderer/index.js': + import Application from './Application.js' ; + Dome.setContent( Application ); + ``` + */ + +import _ from 'lodash'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import type Emitter from 'events'; +import { AppContainer } from 'react-hot-loader'; +import { remote, ipcRenderer } from 'electron'; +import SYS, * as System from 'dome/system'; +import * as Json from 'dome/data/json'; +import * as Settings from 'dome/data/settings'; +import './style.css'; + +// -------------------------------------------------------------------------- +// --- Context +// -------------------------------------------------------------------------- + +// main window focus +var focus = true; + +function setContextAppNode() { + const node = document.getElementById('app'); + if (node) { + node.className = + 'dome-container dome-platform-' + System.platform + + (focus ? ' dome-window-active' : ' dome-window-inactive'); + } + return node; +} + +// -------------------------------------------------------------------------- +// --- Helpers +// -------------------------------------------------------------------------- + +/** Configured to be `'true'` when in development mode. */ +export const DEVEL = System.DEVEL; + +export type PlatformKind = 'linux' | 'macos' | 'windows'; + +/** System platform. */ +export const platform: PlatformKind = (System.platform as PlatformKind); + +// -------------------------------------------------------------------------- +// --- Application Emitter +// -------------------------------------------------------------------------- + +/** Register a callback on Dome event. */ +export function on( + evt: string, + job: (...args: any[]) => void, +) { System.emitter.on(evt, job); } + +/** Register a callback on Dome event. */ +export function off( + evt: string, + job: (...args: any[]) => void, +) { System.emitter.off(evt, job); } + +/** Emit a Dome event (Same as [[dome/misc/system.event]]). */ +export function emit( + evt: string, + ...args: any[] +) { System.emitter.emit(evt, ...args); } + +// -------------------------------------------------------------------------- +// --- Application Events +// -------------------------------------------------------------------------- + +/** Emits the `dome.update` event. */ +export function update() { emit('dome.update'); } + +/** Update event handler. */ +export function onUpdate(job: () => void) { on('dome.update', job); } + +/** Unregister an update event handler. */ +export function offUpdate(job: () => void) { off('dome.update', job); } + +/** Reload event handler. */ +export function onReload(job: () => void) { on('dome.reload', job); } +ipcRenderer.on('dome.ipc.reload', () => emit('dome.reload')); + +/** Command-line arguments event handler. */ +export function onCommand( + job: (argv: string[], workingDir: string) => void +) { on('dome.command', job); } +ipcRenderer.on('dome.ipc.command', (_event, argv, wdir) => { + SYS.SET_COMMAND(argv, wdir); + emit('dome.command', argv, wdir); +}); + +// -------------------------------------------------------------------------- +// --- Window Management +// -------------------------------------------------------------------------- + +export function isApplicationWindow() { + return process.argv.includes(SYS.WINDOW_APPLICATION_ARGV); +} + +export function isPreferencesWindow() { + return process.argv.includes(SYS.WINDOW_PREFERENCES_ARGV); +} + +// -------------------------------------------------------------------------- +// --- Window Title +// -------------------------------------------------------------------------- + +/** Sets the modified status of the window-frame flag. + User feedback is platform dependent. */ +export function setModified(modified = false) { + ipcRenderer.send('dome.ipc.window.modified', modified); +} + +/** Sets the window-frame title. */ +export function setTitle(title: string) { + ipcRenderer.send('dome.ipc.window.title', title); +} + +// -------------------------------------------------------------------------- +// --- Window Container +// -------------------------------------------------------------------------- + +function setContainer( + Component: React.FunctionComponent | React.ComponentClass +) { + Settings.synchronize(); + const appNode = setContextAppNode(); + const appContents = React.createElement(Component); + const appContainer = React.createElement(AppContainer, {}, [appContents]); + ReactDOM.render(appContainer, appNode); +} + +// -------------------------------------------------------------------------- +// --- Main Content +// -------------------------------------------------------------------------- + +/** + Defines the user's main window content. + + Binds the component to the main window. + <strong>Notes:</strong> a `<Component/>` instance is generated and rendered in the `#app` + window element. Its class name is set to `dome-platform-<platform>` with + the `<platform>` set to the `Dome.platform` value. This class name can be used + as a CSS selector for platform-dependent styling. + + @param Component - to be rendered in the main window +*/ +export function setApplicationWindow( + Component: React.FunctionComponent | React.ComponentClass +) { + if (isApplicationWindow()) setContainer(Component); +} + +// -------------------------------------------------------------------------- +// --- Settings Window +// -------------------------------------------------------------------------- + +/** + Defines the user's preferences window content. + + <strong>Notes:</strong> a `<Component/>` instance is generated and rendered in the `#app` + window element. Its class name is set to `dome-platform-<platform>` with + the `<platform>` set to the `Dome.platform` value. This class name can be used + as a CSS selector for platform-dependent styling. + + @param Component - to be rendered in the preferences window +*/ +export function setPreferencesWindow( + Component: React.FunctionComponent | React.ComponentClass +) { + if (isPreferencesWindow()) setContainer(Component); +} + +// -------------------------------------------------------------------------- +// --- MenuBar Management +// -------------------------------------------------------------------------- + +const customItemCallbacks = new Map<string, (() => void)>(); + +/** + Create a new custom menu in the menu bar. + + This function can be triggered at any time, and will eventually trigger + an update of the whole application menubar. + + It is also possible to call this function from the main process. + + @param label - the menu title (shall be unique) +*/ +export function addMenu(label: string) { + ipcRenderer.send('dome.ipc.menu.addmenu', label); +} + +export type MenuName = 'File' | 'Edit' | 'View' | string; +export type MenuItemType = 'normal' | 'separator' | 'checkbox' | 'radio'; + +export interface MenuItemProps { + /** The label of the menu to insert the item in. */ + menu: MenuName; + /** The menu item identifier. Shall be unique in the _entire_ menu bar. */ + id: string; + /** Default is `'normal'`. */ + type: MenuItemType; + /** Item label. Only optional for separators. */ + label?: string; + /** Item is visible or not (default is `true`). */ + visible?: boolean; + /** Enabled item (default is `true`). */ + enabled?: boolean; + /** Item status for radio and checkbox. Default is `false`. */ + checked?: boolean; + /** Keyboard shortcut. */ + key?: string; + /** Callback. */ + onClick?: () => void; +} + +/** + Inserts a new custom item in a menu. + + The menu can be modified later with [[setMenuItem]]. + + When clicked, the menu-item will also trigger a `'dome.menu.clicked'(id)` + event on all application windows. The item callback, if any, is invoked only + in the process that specify it. + + Key short cuts shall be specified with the following codes: + - `"Cmd+<Key>"` for command (MacOS) or control (Linux) key + - `"Alt+<Key>"` for command+option (MacOS) or alt (Linux) key + - `"Meta+<Key>"` for command+shift (MacOS) or control+alt (Linux) key + + This function can be triggered at any time, and will eventually trigger + an update of the complete application menubar. + It is also possible to call this function from the main process. +*/ +export function addMenuItem(props: MenuItemProps) { + if (!props.id && props.type !== 'separator') { + console.error('[Dome] Missing menu-item identifier', props); + return; + } + const { onClick, ...options } = props; + if (onClick) customItemCallbacks.set(props.id, onClick); + ipcRenderer.send('dome.ipc.menu.addmenuitem', options); +} + +export interface MenuItemOptions { + id: string; + label?: string; + visible?: boolean; + enabled?: boolean; + checked?: boolean; + onClick?: null | (() => void); +} + +/** + Update properties of an existing menu-item. + + If an `onClick` callback is specified, it will _replace_ the previous one. + You shall specify `null` to remove the previously registered callback + (`undefined` callback is ignored). + + This function can be triggered at any time, and will possibly trigger + an update of the application menubar if the properties + can not be changed dynamically in Electron. + + It is also possible to call this function from the main process. + */ +export function setMenuItem(options: MenuItemOptions) { + const { onClick, ...updates } = options; + if (onClick === null) { + customItemCallbacks.delete(options.id); + } else if (onClick !== undefined) { + customItemCallbacks.set(options.id, onClick); + } + ipcRenderer.send('dome.ipc.menu.setmenuitem', updates); +} + +ipcRenderer.on('dome.ipc.menu.clicked', (_sender, id: string) => { + const callback = customItemCallbacks.get(id); + callback && callback(); +}); + +// -------------------------------------------------------------------------- +// --- Context Menus +// -------------------------------------------------------------------------- + +export interface PopupMenuItemProps { + /** Item label. */ + label: string; + /** Optional menu identifier. */ + id?: string; + /** Displayed item, default is `true`. */ + display?: boolean; + /** Enabled item, default is `true`. */ + enabled?: boolean; + /** Checked item, default is `false`. */ + checked?: boolean; + /** Item selection callback. */ + onClick?: (() => void); +} + +export type PopupMenuItem = PopupMenuItemProps | 'separator'; + +/** + Popup a contextual menu. + + Items can be separated by inserting a `'separator'` constant string in the + array. Item identifier and label default to each others. Alternatively, an + item can be specified by a single string that will be used for both its label + and identifier. Undefined or null items are allowed (and skipped). + + The menu is displayed at the current mouse location. The callback is called + with the selected item identifier or label. If the menu popup is canceled by + the user, the callback is called with `undefined`. + + Example: + + * ```ts + * let myPopup = (_evt) => Dome.popupMenu([ …items… ],(id) => … ); + * <div onRightClick={myPopup}>...</div> + * ``` + +*/ +export function popupMenu( + items: PopupMenuItem[], + callback?: (item: string | undefined) => void, +) { + const { Menu, MenuItem } = remote; + const menu = new Menu(); + var selected = ''; + var kid = 0; + items.forEach((item) => { + if (item === 'separator') + menu.append(new MenuItem({ type: 'separator' })); + else if (item) { + const { display = true, enabled, checked } = item; + if (display) { + const label = item.label || '#' + (++kid); + const id = item.id || label; + const click = () => { + selected = id; + item.onClick && item.onClick(); + }; + const type = checked !== undefined ? 'checkbox' : 'normal'; + menu.append(new MenuItem({ label, enabled, type, checked, click })); + } + } + }); + const job = callback ? () => callback(selected) : undefined; + menu.popup({ window: remote.getCurrentWindow(), callback: job }); +} + +// -------------------------------------------------------------------------- +// --- Closing +// -------------------------------------------------------------------------- + +ipcRenderer.on('dome.ipc.closing', System.doExit); + +// -------------------------------------------------------------------------- +// --- Focus Management +// -------------------------------------------------------------------------- + +/** Current focus state of the main window. See also [[useWindowFocus]]. */ +export function isFocused() { return focus; } + +ipcRenderer.on('dome.ipc.focus', (_sender, value) => { + focus = value; + setContextAppNode(); + System.emitter.emit('dome.focus', value); +}); + +/** Return the current window focus. See [[isfocused]]. */ +export function useWindowFocus(): boolean { + useUpdate('dome.focus'); + return focus; +} + +// -------------------------------------------------------------------------- +// --- Web Navigation +// -------------------------------------------------------------------------- + +ipcRenderer.on( + 'dome.ipc.href', + (href) => System.emitter.emit('dome.href', href) +); + +/** + Register a callback to handle clicks on a local `<a href=...>` + with non-http protocoles. + + URL with an `http://` protocole are opened externally + by the user's default browser. + + Other URLs shall be treated by the application _via_ this callback. +*/ +export function onDOMhref(callback: (href: string) => void) { + System.emitter.on('dome.href', callback); +} + +// -------------------------------------------------------------------------- +// --- React Hooks +// -------------------------------------------------------------------------- + +/** + Hook to re-render on demand (Custom React Hook). + Returns a callback to trigger a render on demand. +*/ +export function useForceUpdate() { + const [tac, onTic] = React.useState(false); + return () => onTic(!tac); +} + +/** + Hook to re-render on Dome events (Custom React Hook). + @param events - event names, defaults to a single `'dome.update'`. +*/ +export function useUpdate(...events: string[]) { + const update = useForceUpdate(); + React.useEffect(() => { + const trigger = () => setImmediate(update); + if (events.length == 0) events.push('dome.update'); + events.forEach((evt) => System.emitter.on(evt, trigger)); + return () => events.forEach((evt) => System.emitter.off(evt, trigger)); + }); +} + +/** + Hook to register callbacks to Dome events (Custom React Hook). + + Register the callback on event until the component is unmount. + Do not force the component to re-render (unless the callback does). + + @param event - Event to register on + @param callback - The callback to register +*/ +export function useEvent(event: string, callback: () => void) { + React.useEffect(() => { + System.emitter.on(event, callback); + return () => { System.emitter.off(event, callback); }; + }); +} + +/** + Hook to register callbacks to events on an emitter (Custom React Hook). + Similar to [[useEvent]]. +*/ +export function useEmitter( + emitter: Emitter, + evt: string, + callback: () => void, +) { + React.useEffect(() => { + emitter.on(evt, callback); + return () => { emitter.off(evt, callback); }; + }); +} + +// -------------------------------------------------------------------------- +// --- Commands Hooks +// -------------------------------------------------------------------------- + +/** + Hook for command-line interface (Custom React Hook). + Returns the command-line arguments and working directory for the application + instance running in the window. Automatically updated on `dome.command` events. + + @returns `[argv,wdir]` command-line arguments and working directory + + See also [[onCommand]] event handler. +*/ +export function useCommand(): [string[], string] { + useUpdate('dome.command'); + const wdir = System.getWorkingDir(); + const argv = System.getArguments(); + return [argv, wdir]; +} + +// -------------------------------------------------------------------------- +// --- Timer Hooks +// -------------------------------------------------------------------------- + +interface Clock { + timer?: NodeJS.Timeout; + pending: number; // Number of listeners + time: number; // Ellapsed time since firts pending + event: string; // Tic events + period: number; // Period +}; + +// Collection of clocks indexed by period +const CLOCKS = new Map<number, Clock>(); + +const CLOCKEVENT = (period: number) => 'dome.clock.' + period; + +const TIC_CLOCK = (clk: Clock) => () => { + if (0 < clk.pending) { + clk.time += clk.period; + System.emitter.emit(clk.event, clk.time); + } else { + if (clk.timer) clearInterval(clk.timer); + CLOCKS.delete(clk.period); + } +}; + +const INC_CLOCK = (period: number) => { + let clk = CLOCKS.get(period); + if (!clk) { + let event = CLOCKEVENT(period); + let time = (new Date()).getTime(); + clk = { pending: 0, time, period, event }; + clk.timer = setInterval(TIC_CLOCK(clk), period); + CLOCKS.set(period, clk); + } + clk.pending++; + return clk.event; +}; + +const DEC_CLOCK = (period: number) => { + let clk = CLOCKS.get(period); + if (clk) clk.pending--; +}; + +export interface Timer { + /** Starts the timer, if not yet. */ + start(): void; + /** Stops the timer. Can be restarted after. */ + stop(): void; + /** Elapsed time (in milliseconds). */ + time: number; + /** Running timer. */ + running: boolean; +} + +/** + Synchronized start & stop timer (Custom React Hook). + + Create a local timer, synchronized on a global clock, that can be started + and stopped on demand during the life cycle of the component. + + Each timer has its individual start & stop state. However, + all timers with the same period _are_ synchronized with each others. + + @param period - timer interval, in milliseconds (ms) + @param initStart - whether to initially start the timer (default is `false`) + + */ +export function useClock(period: number, initStart: boolean): Timer { + const started = React.useRef(0); + const [time, setTime] = React.useState(0); + const [running, setRunning] = React.useState(initStart); + const start = React.useCallback(() => setRunning(false), []); + const stop = React.useCallback(() => { + setRunning(false); + setTime(0); + started.current = 0; + }, []); + React.useEffect(() => { + if (running) { + const event = INC_CLOCK(period); + const callback = (t: number) => { + if (!started.current) started.current = t; + else setTime(t - started.current); + }; + System.emitter.on(event, callback); + return () => { + System.emitter.off(event, callback); + DEC_CLOCK(period); + }; + } else + return undefined; + }, [running]); + return { time, running, start, stop }; +} + +// -------------------------------------------------------------------------- +// --- Settings Hookds +// -------------------------------------------------------------------------- + +export type FlipState = [boolean, (newState?: boolean) => void]; + +/** + Bool window settings helper. Default is `false` unless specified. + See also [[dome/data/settings]]. + @returns `[state,flipState]` where flipState can be invoked with an + optional argument. By default, `flipState()` invert the state and + `flipState(s)` set the state to `s`. +*/ +export function useBoolSettings( + key: string | undefined, + defaultValue = false, +): FlipState { + const [state, setState] = Settings.useWindowSettings( + key, Json.jBoolean, defaultValue + ); + const flipState = React.useCallback( + (v) => setState(v === undefined ? !state : v), + [state, setState] + ); + return [state, flipState]; +} + +/** Number window settings helper. Default is `0` unless specified. */ +export function useNumberSettings(key: string | undefined, defaultValue = 0) { + return Settings.useWindowSettings( + key, Json.jNumber, defaultValue + ); +} + +/** String window settings. Default is `''` unless specified). */ +export function useStringSettings(key: string | undefined, defaultValue = '') { + return Settings.useWindowSettings( + key, Json.jString, defaultValue + ); +} + +/** Optional string window settings. Default is `undefined`. */ +export function useStringOptSettings(key: string | undefined) { + return Settings.useWindowSettings( + key, Json.jString, undefined + ); +} + +// -------------------------------------------------------------------------- +// --- Pretty Printing (Browser Console) +// -------------------------------------------------------------------------- + +export class Debug { + moduleName: string; + constructor(moduleName: string) { + this.moduleName = moduleName; + } + log(...args: any) { + if (DEVEL) console.log(`[${this.moduleName}]`, ...args); + } + warn(...args: any) { + if (DEVEL) console.warn(`[${this.moduleName}]`, ...args); + } + error(...args: any) { + if (DEVEL) console.error(`[${this.moduleName}]`, ...args); + } +} + +// -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/frame/sidebars.js b/ivette/src/dome/src/renderer/frame/sidebars.js index 6fcd8e42a50b7fca2f0dff2261ebd09b4daf413b..3ee74333bc29cb5eca0b43ba037945c1a613110c 100644 --- a/ivette/src/dome/src/renderer/frame/sidebars.js +++ b/ivette/src/dome/src/renderer/frame/sidebars.js @@ -8,7 +8,7 @@ */ import React from 'react' ; -import * as Dome from 'dome' ; +import { useBoolSettings } from 'dome'; import { Badge } from 'dome/controls/icons' ; import { Label } from 'dome/controls/labels' ; @@ -107,9 +107,8 @@ const disableAll = (children) => export function Section(props) { const context = React.useContext( SideBarContext ); - const [ state=true, setState ] = Dome.useState( - makeSettings(context.settings,props), - props.defaultUnfold + const [ state=true, setState ] = useBoolSettings( + makeSettings(context.settings,props), props.defaultUnfold ); const { enabled=true, disabled=false, unfold, children } = props ; diff --git a/ivette/src/dome/src/renderer/layout/boxes.js b/ivette/src/dome/src/renderer/layout/boxes.js index e7848a9ddcc671c3b9c0dfeb80a733708875357d..a53800adcfb36d77fd00682cb14fcdf855b43eef 100644 --- a/ivette/src/dome/src/renderer/layout/boxes.js +++ b/ivette/src/dome/src/renderer/layout/boxes.js @@ -38,7 +38,7 @@ */ import React from 'react'; -import * as Dome from 'dome'; +import { useBoolSettings } from 'dome'; import { Title } from 'dome/controls/labels' ; import './style.css' ; @@ -168,12 +168,13 @@ export const Grid = ({columns='auto',...props}) => export const Folder = ({ settings, defaultUnfold=false, indent=18, label, title, children }) => { - const [ unfold , setUnfold ] = Dome.useState( settings, defaultUnfold ); + const [ unfold , flipUnfold ] = useBoolSettings( settings, defaultUnfold ); const icon = unfold ? 'TRIANGLE.DOWN' : 'TRIANGLE.RIGHT' ; - const onClick = () => setUnfold( !unfold ); return ( <Vpack> - <Hpack onClick={onClick}><Title icon={icon} label={label} title={title} /></Hpack> + <Hpack onClick={flipUnfold}> + <Title icon={icon} label={label} title={title} /> + </Hpack> <Vpack style={{ marginLeft:indent }}> { unfold && children } </Vpack> diff --git a/ivette/src/dome/src/renderer/layout/forms.js b/ivette/src/dome/src/renderer/layout/forms.js index 3a0d3a9010bb3e786488577602e1960bd415d4e4..468963396f2fc5c9d2476a1bec04c7159c5bd716 100644 --- a/ivette/src/dome/src/renderer/layout/forms.js +++ b/ivette/src/dome/src/renderer/layout/forms.js @@ -9,7 +9,7 @@ import _ from 'lodash' ; import React from 'react' ; -import * as Dome from 'dome' ; +import { useBoolSettings } from 'dome'; import { SVG } from 'dome/controls/icons' ; import { Checkbox, Radio, Select as Selector } from 'dome/controls/buttons' ; import './style.css' ; @@ -400,9 +400,7 @@ const TITLE_DISABLED = 'dome-text-title dome-disabled' ; export function Section(props) { - let [ unfold, setUnfold ] = Dome.useState(props.settings,props.unfold); - - const onSwitch = () => setUnfold(!unfold); + let [ unfold, flipUnfold ] = useBoolSettings(props.settings,props.unfold); const { label, title, path, children, ...otherProps } = props ; @@ -411,7 +409,7 @@ export function Section(props) {(context) => ( <React.Fragment> <div className='dome-xForm-section'> - <div className='dome-xForm-fold' onClick={onSwitch}> + <div className='dome-xForm-fold' onClick={flipUnfold}> <SVG id={unfold?'TRIANGLE.DOWN':'TRIANGLE.RIGHT'} size={11}/> </div> <label className={ (path && context.disabled) ? TITLE_DISABLED : TITLE_ENABLED } diff --git a/ivette/src/dome/src/renderer/layout/grids.js b/ivette/src/dome/src/renderer/layout/grids.js index dd38a71915d290b50a82601b796ce8a9152e1938..bab058a6ef71d174318bf400125ea3909684a455 100644 --- a/ivette/src/dome/src/renderer/layout/grids.js +++ b/ivette/src/dome/src/renderer/layout/grids.js @@ -9,6 +9,8 @@ import _ from 'lodash' ; import React from 'react' ; +import * as Json from 'dome/data/json'; +import * as Settings from 'dome/data/settings'; import { dispatchEvent, DnD, DragSource, DropTarget } from 'dome/dnd' ; import { AutoSizer } from 'react-virtualized' ; import { DraggableCore } from 'react-draggable' ; @@ -1179,11 +1181,11 @@ export class GridLayout extends React.Component } componentDidMount() { - Dome.on('dome.defaults',this.onReloadSettings); + Dome.on('dome.settings.window',this.onReloadSettings); } componentWillUnmont() { - Dome.off('dome.defaults',this.onReloadSettings); + Dome.off('dome.settings.window',this.onReloadSettings); } computeTargetProposal(target) { @@ -1226,7 +1228,7 @@ export class GridLayout extends React.Component onReloadSettings() { const { settings, onReshape } = this.props ; if (!settings) return; - const newShape = Dome.getWindowSetting( settings ); + const newShape = Settings.getWindowSettings( settings, Json.jAny, undefined ); this.setState({ shape: newShape }); if (onReshape) onReshape( newShape ); } @@ -1234,7 +1236,7 @@ export class GridLayout extends React.Component onReshape(newShape) { const { settings, shape:setShape, onReshape } = this.props ; if (!setShape) this.setState({ shape: newShape }); - if (settings) Dome.setWindowSetting( settings, newShape ); + if (settings) Settings.setWindowSettings( settings, newShape ); if (onReshape) onReshape( newShape ); } @@ -1299,7 +1301,8 @@ export class GridLayout extends React.Component const setShape = propShape === null ? {} : propShape ; const insert = inserted ? inserted.id : undefined ; const render = DRAGGABLEITEM(this.dnd,anchor,this.onSelfDrag,insert); - const shape = holdedShape || setShape || newShape || Dome.getWindowSetting(settings) ; + const shape = holdedShape || setShape || newShape || + Settings.getWindowSettings(settings,Json.jAny,undefined) ; return ( <DropTarget dnd={this.dnd} diff --git a/ivette/src/dome/src/renderer/layout/splitters.js b/ivette/src/dome/src/renderer/layout/splitters.js index b6468165cfcf2a07a4e425d291150a7c1a8b8527..14c0cbde86e8443dc97933ab8e64a5b47069aef0 100644 --- a/ivette/src/dome/src/renderer/layout/splitters.js +++ b/ivette/src/dome/src/renderer/layout/splitters.js @@ -10,6 +10,8 @@ import _ from 'lodash' ; import * as React from 'react' ; import * as Dome from 'dome' ; +import * as Json from 'dome/data/json'; +import * as Settings from 'dome/data/settings'; import { Layout } from 'dome/misc/layout' ; import './style.css' ; @@ -165,22 +167,12 @@ export class Splitter extends React.Component { // -------------------------------------------------------------------------- componentDidMount() { - if (this.refs.container && this.refs.splitter) { - const container = this.refs.container.getBoundingClientRect() ; - const dimension = this.lr ? container.width : container.height ; - this.range = dimension - this.margin ; - const settings = this.props.settings ; - if (settings) { - const offset = Dome.getWindowSetting(settings,-1); - if (this.margin <= offset && offset <= this.range) - this.setState({ absolute: true, offset }); - } - } - Dome.on( 'dome.defaults', this.handleReset ); + this.handleReload(); + Dome.on( 'dome.settings.window', this.handleReload ); } componentWillUnmount() { - Dome.off( 'dome.defaults', this.handleReset ); + Dome.off( 'dome.settings.window', this.handleReload ); } componentDidUpdate() { @@ -194,7 +186,7 @@ export class Splitter extends React.Component { handleReset() { this.setState({ absolute: false, dragging: false, anchor: 0, offset: 0 }); - Dome.setWindowSetting(this.props.settings, -1); + Settings.setWindowSettings(this.props.settings, -1); } handleClick(event) { @@ -202,6 +194,20 @@ export class Splitter extends React.Component { this.handleReset(); } + handleReload() { + if (this.refs.container && this.refs.splitter) { + const container = this.refs.container.getBoundingClientRect() ; + const dimension = this.lr ? container.width : container.height ; + this.range = dimension - this.margin ; + const settings = this.props.settings ; + if (settings) { + const offset = Settings.getWindowSettings(settings,JSON.jNumber,-1); + if (this.margin <= offset && offset <= this.range) + this.setState({ absolute: true, offset }); + } + } + } + // -------------------------------------------------------------------------- // --- Resizing Handler // -------------------------------------------------------------------------- @@ -285,7 +291,7 @@ export class Splitter extends React.Component { // -------------------------------------------------------------------------- handleDragStop(evt) { - Dome.setWindowSetting( this.props.settings, this.state.offset ); + Settings.setWindowSettings( this.props.settings, this.state.offset ); this.setState({ dragging: false }); } diff --git a/ivette/src/dome/src/renderer/table/views.tsx b/ivette/src/dome/src/renderer/table/views.tsx index 5e7888b7c89035af4b1b8de6cfa738f47a075fdd..4efa8a5b090320cf4c72f4794dbfdfbe741d4b37 100644 --- a/ivette/src/dome/src/renderer/table/views.tsx +++ b/ivette/src/dome/src/renderer/table/views.tsx @@ -11,6 +11,8 @@ import React, { ReactNode } from 'react'; import { forEach, debounce, throttle } from 'lodash'; import isEqual from 'react-fast-compare'; import * as Dome from 'dome'; +import * as Json from 'dome/data/json'; +import * as Settings from 'dome/data/settings'; import { DraggableCore } from 'react-draggable'; import { AutoSizer, Size, @@ -254,13 +256,16 @@ function makeDataRenderer( // --- Table Settings // -------------------------------------------------------------------------- -type ColSettings<A> = { [id: string]: undefined | null | A }; - type TableSettings = { - resize?: ColSettings<number>; - visible?: ColSettings<boolean>; + resize?: Json.dict<number>; + visible?: Json.dict<boolean>; } +const jTableSettings = Json.jObject({ + resize: Json.jDict(Json.jNumber), + visible: Json.jDict(Json.jBoolean), +}); + // -------------------------------------------------------------------------- // --- Table State // -------------------------------------------------------------------------- @@ -305,7 +310,7 @@ class TableState<Key, Row> { this.onRowRightClick = this.onRowRightClick.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.onSorting = this.onSorting.bind(this); - this.clearSettings = this.clearSettings.bind(this); + this.reloadSettings = this.reloadSettings.bind(this); this.rebuild = debounce(this.rebuild.bind(this), 5); this.rowGetter = makeRowGetter(); } @@ -383,49 +388,48 @@ class TableState<Key, Row> { // --- Table settings - clearSettings() { - this.resize.clear(); - this.visible.clear(); - this.forceUpdate(); - } - updateSettings() { const userSettings = this.settings; if (userSettings) { - const cws: ColSettings<number> = {}; - const cvs: ColSettings<boolean> = {}; + const cws: Json.dict<number> = {}; + const cvs: Json.dict<boolean> = {}; const resize = this.resize; const visible = this.visible; this.columns.forEach(({ id }) => { const cw = resize.get(id); const cv = visible.get(id); - cws[id] = cw === undefined ? null : cw; - cvs[id] = cv === undefined ? null : cv; + if (cw) cws[id] = cw; + if (cv) cvs[id] = cv; }); const theSettings: TableSettings = { resize: cws, visible: cvs }; - Dome.setWindowSetting(userSettings, theSettings); + Settings.setWindowSettings(userSettings, theSettings); } this.forceUpdate(); } + reloadSettings() { + const settings = this.settings; + const resize = this.resize; + const visible = this.visible; + resize.clear(); + visible.clear(); + const theSettings: undefined | TableSettings = + Settings.getWindowSettings(settings, jTableSettings, undefined); + if (theSettings) { + forEach(theSettings.resize, (cw, cid) => { + this.resize.set(cid, cw); + }); + forEach(theSettings.visible, (cv, cid) => { + this.visible.set(cid, cv); + }); + this.forceUpdate(); + } + } + importSettings(settings?: string) { if (settings !== this.settings) { this.settings = settings; - const resize = this.resize; - const visible = this.visible; - resize.clear(); - visible.clear(); - const theSettings: undefined | TableSettings = - Dome.getWindowSetting(settings); - if (theSettings) { - forEach(theSettings.resize, (cw, cid) => { - if (typeof cw === 'number') this.resize.set(cid, cw); - }); - forEach(theSettings.visible, (cv, cid) => { - if (typeof cv === 'boolean') this.visible.set(cid, cv); - }); - this.forceUpdate(); - } + this.reloadSettings(); } } @@ -1083,7 +1087,7 @@ export function Table<Key, Row>(props: TableProps<Key, Row>) { state.onContextMenu = props.onContextMenu; return state.unwind; }); - Dome.useEvent('dome.defaults', state.clearSettings); + Dome.useEvent('dome.settings.window', state.reloadSettings); return ( <div className='dome-xTable'> <React.Fragment key='columns'> diff --git a/ivette/src/frama-c/LabViews.tsx b/ivette/src/frama-c/LabViews.tsx index 52bc3151cac0951f6f9642dee1e7ceb83b05dc6b..810996d572907d0c1fffc7511e46bc9f45b1ec7b 100644 --- a/ivette/src/frama-c/LabViews.tsx +++ b/ivette/src/frama-c/LabViews.tsx @@ -10,6 +10,8 @@ import _ from 'lodash'; import React from 'react'; import * as Dome from 'dome'; +import * as Json from 'dome/data/json'; +import * as Settings from 'dome/data/settings'; import { Catch } from 'dome/errors'; import { DnD, DragSource } from 'dome/dnd'; import { SideBar, Section, Item } from 'dome/frame/sidebars'; @@ -349,9 +351,13 @@ const makeGridItem = (customize: any, onClose: any) => (comp: any) => { // --- Customization Views // -------------------------------------------------------------------------- +const Stock = new Settings.GDefault('frama-c.labView', Json.jAny, {}); + function CustomViews({ settings, shape, setShape, views: libViews }: any) { - const [local, setLocal] = Dome.useState(settings, {}); - const [customs, setCustoms] = Dome.useGlobalSetting(settings, {}); + const [local, setLocal] = Settings.useWindowSettings( + settings, Json.jAny, {}, + ) as any; + const [customs, setCustoms] = Settings.useGlobalSettings(Stock) as any; const [edited, setEdited]: any = React.useState(); const triggerDefault = React.useRef(); const { current, shapes = {} } = local; @@ -633,10 +639,15 @@ export function LabView(props: any) { const settingShape = settings && `${settings}.shape`; const settingPanel = settings && `${settings}.panel`; // Hooks & State - Dome.useUpdate('labview.library', 'dome.defaults'); + Dome.useUpdate( + 'labview.library', + 'dome.settings.window', + 'dome.settings.global', + ); const dnd = React.useMemo(() => new DnD(), []); const lib = React.useMemo(() => new Library(), []); - const [shape, setShape] = Dome.useState(settingShape); + const [shape, setShape] = + Settings.useWindowSettings(settingShape, Json.jAny, undefined); const [dragging, setDragging] = React.useState(); // Preparation const onClose = diff --git a/ivette/src/frama-c/server.ts b/ivette/src/frama-c/server.ts index d400a9745fe2cb5429a904414641a6def97d3a0e..51190f03fdc8debdf08ab53104736a44a02d1a76 100644 --- a/ivette/src/frama-c/server.ts +++ b/ivette/src/frama-c/server.ts @@ -21,7 +21,7 @@ import { ChildProcess } from 'child_process'; // --- Pretty Printing (Browser Console) // -------------------------------------------------------------------------- -const PP = new Dome.PP('Server'); +const D = new Dome.Debug('Server'); // -------------------------------------------------------------------------- // --- Events @@ -223,7 +223,7 @@ export function onActivity(signal: string, callback: any) { function _status(newStatus: Status) { if (Dome.DEVEL && hasErrorStatus(newStatus)) { - PP.error(newStatus.error); + D.error(newStatus.error); } if (newStatus !== status) { @@ -256,7 +256,7 @@ export async function start() { await _launch(); _status(okStatus(Stage.ON)); } catch (error) { - PP.error(error.toString()); + D.error(error.toString()); buffer.append(error.toString(), '\n'); _exit(error); } @@ -484,7 +484,7 @@ async function _launch() { process?.stdout?.on('data', logger); process?.stderr?.on('data', logger); process?.on('exit', (code: number | null, signal: string | null) => { - PP.log('Process exited'); + D.log('Process exited'); if (signal) { // [signal] is non-null. @@ -514,7 +514,7 @@ async function _launch() { // -------------------------------------------------------------------------- function _reset() { - PP.log('Reset to initial configuration'); + D.log('Reset to initial configuration'); rqCount = 0; queueCmd = []; @@ -536,7 +536,7 @@ function _reset() { } function _kill() { - PP.log('Hard kill'); + D.log('Hard kill'); _reset(); if (process) { @@ -545,7 +545,7 @@ function _kill() { } async function _shutdown() { - PP.log('Shutdown'); + D.log('Shutdown'); _reset(); queueCmd.push('SHUTDOWN'); @@ -602,7 +602,7 @@ class SignalHandler { } on(callback: any) { - const n = Dome.emitter.listenerCount(this.event); + const n = System.emitter.listenerCount(this.event); Dome.on(this.event, callback); if (n === 0) { this.active = true; @@ -612,7 +612,7 @@ class SignalHandler { off(callback: any) { Dome.off(this.event, callback); - const n = Dome.emitter.listenerCount(this.event); + const n = System.emitter.listenerCount(this.event); if (n === 0) { this.active = false; if (isRunning()) this.sigoff(); @@ -846,7 +846,7 @@ async function _send() { const resp = await zmqSocket?.receive(); _receive(resp); } catch (error) { - PP.error(`Error in send/receive on ZMQ socket. ${error.toString()}`); + D.error(`Error in send/receive on ZMQ socket. ${error.toString()}`); _cancel(ids); } zmqIsBusy = false; @@ -895,10 +895,10 @@ function _receive(resp: any) { break; case 'WRONG': err = shift(); - PP.error(`ZMQ Protocol Error: ${err}`); + D.error(`ZMQ Protocol Error: ${err}`); break; default: - PP.error(`Unknown Response: ${cmd}`); + D.error(`Unknown Response: ${cmd}`); unknownResponse = true; break; } diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index 3474e9acd6cd3c21998f410e6a8a3d1381e1733f..f1e6d91c15b70cd82d413ce414967bb4d45ed9f1 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -25,7 +25,7 @@ const STATE_PREFIX = 'frama-c.state.'; // --- Pretty Printing (Browser Console) // -------------------------------------------------------------------------- -const PP = new Dome.PP('States'); +const D = new Dome.Debug('States'); // -------------------------------------------------------------------------- // --- Synchronized Current Project @@ -45,7 +45,7 @@ Server.onReady(async () => { currentProject = current.id; Dome.emit(PROJECT); } catch (error) { - PP.error(`Fail to retrieve the current project. ${error.toString()}`); + D.error(`Fail to retrieve the current project. ${error.toString()}`); } }); @@ -85,7 +85,7 @@ export async function setProject(project: string) { currentProject = project; Dome.emit(PROJECT); } catch (error) { - PP.error(`Fail to set the current project. ${error.toString()}`); + D.error(`Fail to set the current project. ${error.toString()}`); } } } @@ -131,7 +131,7 @@ export function useRequest<In, Out>( const r = await Server.send(rq, params); update(r); } catch (error) { - PP.error(`Fail in useRequest '${rq.name}'. ${error.toString()}`); + D.error(`Fail in useRequest '${rq.name}'. ${error.toString()}`); update(options.onError); } } else { @@ -250,7 +250,7 @@ class SyncState<A> { if (setter) await Server.send(setter, v); Dome.emit(this.UPDATE); } catch (error) { - PP.error( + D.error( `Fail to set value of syncState '${this.handler.name}'.`, `${error.toString()}`, ); @@ -264,7 +264,7 @@ class SyncState<A> { this.value = v; Dome.emit(this.UPDATE); } catch (error) { - PP.error( + D.error( `Fail to update syncState '${this.handler.name}'.`, `${error.toString()}`, ); @@ -311,7 +311,7 @@ export function useSyncState<A>( */ export function useSyncValue<A>(va: Value<A>): A | undefined { const s = getSyncState(va); - Dome.useUpdate(s.update); + Dome.useUpdate(PROJECT, s.UPDATE); Server.useSignal(s.handler.signal, s.update); return s.getValue(); } @@ -361,7 +361,7 @@ class SyncArray<K, A> { } while (pending > 0); /* eslint-enable no-await-in-loop */ } catch (error) { - PP.error( + D.error( `Fail to retrieve the value of syncArray '${this.handler.name}.`, `${error.toString()}`, ); @@ -380,7 +380,7 @@ class SyncArray<K, A> { this.fetch(); } } catch (error) { - PP.error( + D.error( `Fail to set reload of syncArray '${this.handler.name}'.`, `${error.toString()}`, ); diff --git a/ivette/src/renderer/ASTview.tsx b/ivette/src/renderer/ASTview.tsx index a635b1a7869202b426cbf363bf03a009a6194da9..e2463f89692a41ce7a97e300e9ae5c6d6e9885ec 100644 --- a/ivette/src/renderer/ASTview.tsx +++ b/ivette/src/renderer/ASTview.tsx @@ -7,6 +7,7 @@ import * as Server from 'frama-c/server'; import * as States from 'frama-c/states'; import * as Dome from 'dome'; +import * as Settings from 'dome/data/settings'; import { key } from 'dome/data/json'; import { RichTextBuffer } from 'dome/text/buffers'; import { Text } from 'dome/text/editors'; @@ -19,6 +20,8 @@ import 'codemirror/mode/clike/clike'; import 'codemirror/theme/ambiance.css'; import 'codemirror/theme/solarized.css'; +import { Theme, FontSize } from './Preferences'; + const THEMES = [ { id: 'default', label: 'Default' }, { id: 'ambiance', label: 'Ambiance' }, @@ -30,7 +33,7 @@ const THEMES = [ // --- Pretty Printing (Browser Console) // -------------------------------------------------------------------------- -const PP = new Dome.PP('AST View'); +const D = new Dome.Debug('AST View'); // -------------------------------------------------------------------------- // --- Rich Text Printer @@ -55,7 +58,7 @@ async function loadAST( buffer.scroll(theMarker, undefined); }); } catch (err) { - PP.error( + D.error( 'Fail to retrieve the AST of function', theFunction, 'marker:', theMarker, err, ); @@ -74,9 +77,9 @@ const ASTview = () => { const buffer = React.useMemo(() => new RichTextBuffer(), []); const printed: React.MutableRefObject<string | undefined> = React.useRef(); const [selection, updateSelection] = States.useSelection(); - const [theme, setTheme] = Dome.useGlobalSetting('ASTview.theme', 'default'); - const [fontSize, setFontSize] = Dome.useGlobalSetting('ASTview.fontSize', 12); - const [wrapText, setWrapText] = Dome.useSwitch('ASTview.wrapText', false); + const [theme, setTheme] = Settings.useGlobalSettings(Theme); + const [fontSize, setFontSize] = Settings.useGlobalSettings(FontSize); + const [wrapText, flipWrapText] = Dome.useBoolSettings('ASTview.wrapText'); const markers = States.useSyncModel(markerInfo); const theFunction = selection?.current?.function; @@ -122,10 +125,10 @@ const ASTview = () => { // Theme Popup const selectTheme = (id?: string) => id && setTheme(id); - const checkTheme = - (th: { id: string }) => ({ checked: th.id === theme, ...th }); - const themePopup = - () => Dome.popupMenu(THEMES.map(checkTheme), selectTheme); + const themeItem = (th: { id: string; label: string }) => ( + { checked: th.id === theme, ...th } + ); + const themePopup = () => Dome.popupMenu(THEMES.map(themeItem), selectTheme); // Component return ( @@ -151,7 +154,7 @@ const ASTview = () => { <IconButton icon="WRAPTEXT" selected={wrapText} - onClick={setWrapText} + onClick={flipWrapText} title="Wrap text" /> </TitleBar> diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx index 7aee4d0277d9d5bca6044a4e436c220f2006dcbd..a593a1e967ec5e5847105b5f203b8a3b76f066f4 100644 --- a/ivette/src/renderer/Application.tsx +++ b/ivette/src/renderer/Application.tsx @@ -54,14 +54,8 @@ const SelectionControls = () => { // -------------------------------------------------------------------------- export default (() => { - const [sidebar, flipSidebar] = Dome.useSwitch( - 'frama-c.sidebar.unfold', - false, - ); - const [viewbar, flipViewbar] = Dome.useSwitch( - 'frama-c.viewbar.unfold', - false, - ); + const [sidebar, flipSidebar] = Dome.useBoolSettings('frama-c.sidebar.unfold'); + const [viewbar, flipViewbar] = Dome.useBoolSettings('frama-c.viewbar.unfold'); return ( <Vfill> diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx index 699fa98dc8a2018dc87afac4848253179e67aaad..17e78da201562f6c311fc1a94b8a5bd94435b838 100644 --- a/ivette/src/renderer/Controller.tsx +++ b/ivette/src/renderer/Controller.tsx @@ -4,6 +4,8 @@ import React from 'react'; import * as Dome from 'dome'; +import * as Json from 'dome/data/json'; +import * as Settings from 'dome/data/settings'; import { Button as ToolButton, ButtonGroup, Space } from 'dome/frame/toolbars'; import { LED, LEDstatus, IconButton } from 'dome/controls/buttons'; @@ -97,8 +99,10 @@ function insertConfig(hs: string[], cfg: Server.Configuration) { let reloadCommand: string | undefined; Dome.onReload(() => { - const hst = Dome.getWindowSetting('Controller.history'); - reloadCommand = Array.isArray(hst) && hst[0]; + const [lastCmd] = Settings.getWindowSettings( + 'Controller.history', Json.jList(Json.jString), [], + ); + reloadCommand = lastCmd; }); Dome.onCommand((argv: string[], cwd: string) => { @@ -169,9 +173,11 @@ const editor = new RichTextBuffer(); const RenderConsole = () => { const scratch = React.useRef([] as string[]); const [cursor, setCursor] = React.useState(-1); - const [history, setHistory] = Dome.useState('Controller.history', []); const [isEmpty, setEmpty] = React.useState(true); const [noTrash, setNoTrash] = React.useState(true); + const [history, setHistory] = Settings.useWindowSettings( + 'Controller.history', Json.jList(Json.jString), [], + ); Dome.useEmitter(editor, 'change', () => { const cmd = editor.getValue().trim(); diff --git a/ivette/src/renderer/Preferences.js b/ivette/src/renderer/Preferences.js index 7a9d1f76f585e32a17058ba1bd3c7151644aa108..1847aa2dda1a8f4429102222b8fb6034694400ff 100644 --- a/ivette/src/renderer/Preferences.js +++ b/ivette/src/renderer/Preferences.js @@ -14,12 +14,17 @@ import React from 'react' ; import * as Dome from 'dome'; +import * as Json from 'dome/data/json'; +import * as Settings from 'dome/data/settings'; import { Form, Section, FieldSelect, FieldCheckbox, FieldSlider } from 'dome/layout/forms' ; +export const Theme = new Settings.GString('ASTview.theme','default'); +export const FontSize = new Settings.GNumber('ASTview.fontSize',12); + const ASTviewPrefs = () => { - const [theme, setTheme] = Dome.useGlobalSetting('ASTview.theme', 'default'); - const [fontSize, setFontSize] = Dome.useGlobalSetting('ASTview.fontSize', 12); + const [theme, setTheme] = Settings.useGlobalSettings(Theme); + const [fontSize, setFontSize] = Settings.useGlobalSettings(FontSize); return ( <React.Fragment> diff --git a/ivette/src/renderer/Properties.tsx b/ivette/src/renderer/Properties.tsx index e2a434f7a7e0743ff27b63b55aa4ac0d0f9d5fa5..f1722e6816cc6d12dd84aea1baacdf4e1c2ed632 100644 --- a/ivette/src/renderer/Properties.tsx +++ b/ivette/src/renderer/Properties.tsx @@ -5,7 +5,7 @@ import _ from 'lodash'; import React, { useEffect } from 'react'; import * as Dome from 'dome'; -import { key } from 'dome/data/json'; +import * as Json from 'dome/data/json'; import * as States from 'frama-c/states'; import * as Compare from 'dome/data/compare'; import { Label, Code } from 'dome/controls/labels'; @@ -239,7 +239,7 @@ const byColumn: Arrays.ByColumns<Property> = { file: Compare.byFields<Property>({ source: byFile }), }; -class PropertyModel extends Arrays.CompactModel<key<'#status'>, Property> { +class PropertyModel extends Arrays.CompactModel<Json.key<'#status'>, Property> { private filterFun?: string; private filterProp = _.cloneDeep(defaultFilter); @@ -438,10 +438,11 @@ const RenderTable = () => { model.reload(); }, [model, data]); - const [selection, updateSelection] = States.useSelection(); + const [selection, updateSelection] = + States.useSelection(); const [showFilter, flipFilter] = - Dome.useSwitch('ivette.properties.showFilter', true); + Dome.useBoolSettings('ivette.properties.showFilter'); // Updating the filter const selectedFunction = selection?.current?.function;