diff --git a/ivette/src/dome/src/renderer/data/settings.ts b/ivette/src/dome/src/renderer/data/settings.ts index e6e3a8a246e453da738b9ec83e2798b058924b97..3cf6d7cd231d0554031d60b11f9e239fdc42329e 100644 --- a/ivette/src/dome/src/renderer/data/settings.ts +++ b/ivette/src/dome/src/renderer/data/settings.ts @@ -345,7 +345,13 @@ export function useGlobalSettings<A>(S: GlobalSettings<A>) { // --- Settings Synchronization // -------------------------------------------------------------------------- -/*@ internal */ +/* @ internal */ +export const window = WindowDriver.evt; + +/* @ internal */ +export const global = GlobalDriver.evt; + +/* @ internal */ export function synchronize() { ipcRenderer.sendSync( 'dome.ipc.settings.sync', @@ -354,7 +360,8 @@ export function synchronize() { GlobalDriver.sync(globals); const settings: patch[] = data.settings ?? []; WindowDriver.sync(settings); - }); + }, + ); } // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/dome.ts b/ivette/src/dome/src/renderer/dome.ts index ab451cbafcb378f968e2188e9794c8f5d9e51c1f..aac7877c772d10758b62330df706dd11d6b9bfb0 100644 --- a/ivette/src/dome/src/renderer/dome.ts +++ b/ivette/src/dome/src/renderer/dome.ts @@ -1,26 +1,24 @@ /** - @packageDocumentation - @module dome(renderer) - @description - - ## Dome Application (Renderer Process) + 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 ); - ``` + * ```ts + * // File 'src/renderer/index.js': + * import Application from './Application.js' ; + * Dome.setContent( Application ); + * ``` + + @packageDocumentation + @module dome */ 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'; @@ -33,14 +31,14 @@ import './style.css'; // -------------------------------------------------------------------------- // main window focus -var focus = true; +let windowFocus = 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'); + `dome-container dome-platform-${System.platform + }${windowFocus ? ' dome-window-active' : ' dome-window-inactive'}`; } return node; } @@ -50,7 +48,7 @@ function setContextAppNode() { // -------------------------------------------------------------------------- /** Configured to be `'true'` when in development mode. */ -export const DEVEL = System.DEVEL; +export const { DEVEL } = System; export type PlatformKind = 'linux' | 'macos' | 'windows'; @@ -61,50 +59,147 @@ 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); } +/** Typed Dome Event. + + To register an event with no argument, simply use `new Event('myEvent')`. +*/ +export class Event<A = void> { + + private name: string; + + constructor(name: string) { + this.name = name; + this.emit = this.emit.bind(this); + } + + on(callback: (arg: A) => void) { + System.emitter.on(this.name, callback); + } + + off(callback: (arg: A) => void) { + System.emitter.off(this.name, callback); + } + + /** + Notify all listeners with the provided argument. + This methods is bound to the event, so you may use `myEvent.emit` + as a callback function, instead of eg. `(arg) => myEvent.emit(arg)`. + */ + emit(arg: A) { + System.emitter.emit(this.name, arg); + } + + /** + Number of currenty registered listeners. + */ + listenerCount() { + return System.emitter.listenerCount(this.name); + } -/** 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); } +export function useEvent<A>( + evt: Event<A>, + callback: (arg: A) => void, +) { + return React.useEffect(() => { + evt.on(callback); + return () => evt.off(callback); + }); +} // -------------------------------------------------------------------------- // --- 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); } +/** + Dome update event. + It is emitted when a general re-rendering is required, typically when + the window frame is resized. + You can use it for your own components as an easy-to-use global + re-render event. +*/ +export const update = new Event('dome.update'); -/** Unregister an update event handler. */ -export function offUpdate(job: () => void) { off('dome.update', job); } +/** + Dome reload event. + It is emitted when the entire window is reloaded. +*/ +export const reload = new Event('dome.reload'); -/** Reload event handler. */ -export function onReload(job: () => void) { on('dome.reload', job); } -ipcRenderer.on('dome.ipc.reload', () => emit('dome.reload')); +ipcRenderer.on('dome.ipc.reload', () => reload.emit()); /** Command-line arguments event handler. */ export function onCommand( - job: (argv: string[], workingDir: string) => void -) { on('dome.command', job); } + job: (argv: string[], workingDir: string) => void, +) { + System.emitter.on('dome.command', job); +} + ipcRenderer.on('dome.ipc.command', (_event, argv, wdir) => { SYS.SET_COMMAND(argv, wdir); - emit('dome.command', argv, wdir); + System.emitter.emit('dome.command', argv, wdir); +}); + +/** Window Settings event. + Emitted when window settings are reset or restored. */ +export const windowSettings = new Event(Settings.window); + +/** Global Settings event. + Emiited when global settings are updated. */ +export const globalSettings = new Event(Settings.global); + +// -------------------------------------------------------------------------- +// --- Closing +// -------------------------------------------------------------------------- + +ipcRenderer.on('dome.ipc.closing', System.doExit); + +/** Register a callback to be executed when the window is closing. */ +export function atExit(callback: () => void) { + System.atExit(callback); +} + +// -------------------------------------------------------------------------- +// --- Focus Management +// -------------------------------------------------------------------------- + +/** Window focus event. */ +export const focus = new Event<boolean>('dome.focus'); + +/** Current focus state of the main window. See also [[useWindowFocus]]. */ +export function isFocused() { return windowFocus; } + +ipcRenderer.on('dome.ipc.focus', (_sender, value) => { + windowFocus = value; + setContextAppNode(); + focus.emit(value); }); +/** Return the current window focus. See [[isfocused]]. */ +export function useWindowFocus(): boolean { + useUpdate(focus); + return windowFocus; +} + +// -------------------------------------------------------------------------- +// --- Web Navigation +// -------------------------------------------------------------------------- + +/** + DOM href events for internal URLs. + + This event is emitted whenever some `<a href/>` DOM element + is clicked with an internal link. External links will be automatically + opened with the user's default Web navigator. + */ +export const navigate = new Event<string>('dome.href'); + +ipcRenderer.on( + 'dome.ipc.href', + (_sender, href) => navigate.emit(href), +); + // -------------------------------------------------------------------------- // --- Window Management // -------------------------------------------------------------------------- @@ -137,7 +232,7 @@ export function setTitle(title: string) { // -------------------------------------------------------------------------- function setContainer( - Component: React.FunctionComponent | React.ComponentClass + Component: React.FunctionComponent | React.ComponentClass, ) { Settings.synchronize(); const appNode = setContextAppNode(); @@ -153,16 +248,16 @@ function setContainer( /** 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. + Binds the component to the main window. 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 + Component: React.FunctionComponent | React.ComponentClass, ) { if (isApplicationWindow()) setContainer(Component); } @@ -174,15 +269,15 @@ export function setApplicationWindow( /** 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. + 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 + Component: React.FunctionComponent | React.ComponentClass, ) { if (isPreferencesWindow()) setContainer(Component); } @@ -191,7 +286,8 @@ export function setPreferencesWindow( // --- MenuBar Management // -------------------------------------------------------------------------- -const customItemCallbacks = new Map<string, (() => void)>(); +type callback = () => void; +const customItemCallbacks = new Map<string, callback>(); /** Create a new custom menu in the menu bar. @@ -293,7 +389,7 @@ export function setMenuItem(options: MenuItemOptions) { ipcRenderer.on('dome.ipc.menu.clicked', (_sender, id: string) => { const callback = customItemCallbacks.get(id); - callback && callback(); + if (callback) callback(); }); // -------------------------------------------------------------------------- @@ -343,19 +439,19 @@ export function popupMenu( ) { const { Menu, MenuItem } = remote; const menu = new Menu(); - var selected = ''; - var kid = 0; + let selected = ''; + let 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 label = item.label || `#${++kid}`; const id = item.id || label; const click = () => { selected = id; - item.onClick && item.onClick(); + if (item.onClick) item.onClick(); }; const type = checked !== undefined ? 'checkbox' : 'normal'; menu.append(new MenuItem({ label, enabled, type, checked, click })); @@ -366,53 +462,6 @@ export function popupMenu( 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 // -------------------------------------------------------------------------- @@ -430,65 +479,15 @@ export function useForceUpdate() { 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(); +export function useUpdate(...events: Event<any>[]) { + const fn = 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]; + const trigger = () => setImmediate(fn); + if (events.length === 0) events.push(update); + events.forEach((evt) => evt.on(trigger)); + return () => events.forEach((evt) => evt.off(trigger)); + }, [fn, ...events]); // eslint-disable-line react-hooks/exhaustive-deps + // The rule signals events is missing, probably because of « … » } // -------------------------------------------------------------------------- @@ -501,12 +500,12 @@ interface Clock { 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 CLOCKEVENT = (period: number) => `dome.clock.${period}`; const TIC_CLOCK = (clk: Clock) => () => { if (0 < clk.pending) { @@ -521,8 +520,8 @@ const TIC_CLOCK = (clk: Clock) => () => { const INC_CLOCK = (period: number) => { let clk = CLOCKS.get(period); if (!clk) { - let event = CLOCKEVENT(period); - let time = (new Date()).getTime(); + const event = CLOCKEVENT(period); + const time = (new Date()).getTime(); clk = { pending: 0, time, period, event }; clk.timer = setInterval(TIC_CLOCK(clk), period); CLOCKS.set(period, clk); @@ -532,7 +531,7 @@ const INC_CLOCK = (period: number) => { }; const DEC_CLOCK = (period: number) => { - let clk = CLOCKS.get(period); + const clk = CLOCKS.get(period); if (clk) clk.pending--; }; @@ -582,9 +581,8 @@ export function useClock(period: number, initStart: boolean): Timer { System.emitter.off(event, callback); DEC_CLOCK(period); }; - } else - return undefined; - }, [running]); + } return undefined; + }, [period, running]); return { time, running, start, stop }; } @@ -606,11 +604,11 @@ export function useBoolSettings( defaultValue = false, ): FlipState { const [state, setState] = Settings.useWindowSettings( - key, Json.jBoolean, defaultValue + key, Json.jBoolean, defaultValue, ); const flipState = React.useCallback( (v) => setState(v === undefined ? !state : v), - [state, setState] + [state, setState], ); return [state, flipState]; } @@ -618,26 +616,26 @@ export function useBoolSettings( /** Number window settings helper. Default is `0` unless specified. */ export function useNumberSettings(key: string | undefined, defaultValue = 0) { return Settings.useWindowSettings( - key, Json.jNumber, defaultValue + 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 + key, Json.jString, defaultValue, ); } /** Optional string window settings. Default is `undefined`. */ export function useStringOptSettings(key: string | undefined) { return Settings.useWindowSettings( - key, Json.jString, undefined + key, Json.jString, undefined, ); } /** Direct shortcut to [[dome/data/settings.useWindowSettings]]. */ -export const useWindowSettings = Settings.useWindowSettings; +export const { useWindowSettings } = Settings; /** Utility shortcut to [[dome/data/settings.useGlobalSettings]] @@ -646,11 +644,11 @@ export const useWindowSettings = Settings.useWindowSettings; export function useGlobalSettings<A extends Json.json>( globalKey: string, decoder: Json.Loose<A>, - defaultValue: A + defaultValue: A, ) { // Object creation is cheaper than useMemo... const G = new Settings.GlobalSettings( - globalKey, decoder, Json.identity, defaultValue + globalKey, decoder, Json.identity, defaultValue, ); return Settings.useGlobalSettings(G); } @@ -664,15 +662,22 @@ export class Debug { constructor(moduleName: string) { this.moduleName = moduleName; } + + /* eslint-disable no-console */ + 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); } + + /* eslint-enable */ } // -------------------------------------------------------------------------- diff --git a/ivette/src/dome/src/renderer/layout/boxes.tsx b/ivette/src/dome/src/renderer/layout/boxes.tsx index 2d98e79fd1753cd726bf857c092db42f18cb9f52..d95a01e940833421a464cd9844ebd4b14ffd900a 100644 --- a/ivette/src/dome/src/renderer/layout/boxes.tsx +++ b/ivette/src/dome/src/renderer/layout/boxes.tsx @@ -180,12 +180,13 @@ export const Folder = (props: FolderProps) => { indent = 18, label, title, children, } = props; - const [unfold, onClick] = Dome.useSwitch(settings, defaultUnfold); + const [unfold, onClick] = Dome.useBoolSettings(settings, defaultUnfold); + const foldUnfold = React.useCallback(() => onClick(), [onClick]); const icon = unfold ? 'TRIANGLE.DOWN' : 'TRIANGLE.RIGHT'; const display = unfold ? 'none' : 'block'; return ( <Vpack> - <Hpack onClick={onClick}> + <Hpack onClick={foldUnfold}> <Title icon={icon} label={label} title={title} /> </Hpack> <Vpack style={{ display, marginLeft: indent }}> diff --git a/ivette/src/dome/src/renderer/table/views.tsx b/ivette/src/dome/src/renderer/table/views.tsx index 1020db211e4783c372277ea0de322ae2ec2a2931..1adb34a0f4873bfa36115555c614ca388c52d0e9 100644 --- a/ivette/src/dome/src/renderer/table/views.tsx +++ b/ivette/src/dome/src/renderer/table/views.tsx @@ -259,7 +259,7 @@ function makeDataRenderer( type TableSettings = { resize?: Json.dict<number>; visible?: Json.dict<boolean>; -} +}; const jTableSettings = Json.jObject({ resize: Json.jDict(Json.jNumber), @@ -1094,7 +1094,7 @@ export function Table<Key, Row>(props: TableProps<Key, Row>) { state.onContextMenu = props.onContextMenu; return state.unwind; }); - Dome.useEvent('dome.settings.window', state.reloadSettings); + Dome.useEvent(Dome.windowSettings, 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 84f97ec9f14bb70cf38fda5c7077a1a5ee9dafc7..e1465dda5981e8a5a7deeb60ecad5c9a0c40d059 100644 --- a/ivette/src/frama-c/LabViews.tsx +++ b/ivette/src/frama-c/LabViews.tsx @@ -38,6 +38,8 @@ class Library { collection: {}; items: any[]; + static update = new Dome.Event('labview.library'); + constructor() { this.modified = false; this.virtual = {}; @@ -50,7 +52,7 @@ class Library { this.collection = { ...this.virtual }; this.items = _.sortBy(this.collection, ['order', 'id']); this.modified = false; - Dome.emit('labview.library'); + Library.update.emit(); } } @@ -351,13 +353,12 @@ 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] = Settings.useWindowSettings( settings, Json.jAny, {}, ) as any; - const [customs, setCustoms] = Settings.useGlobalSettings(Stock) as any; + const [customs, setCustoms] = + Dome.useGlobalSettings<any>('frama-c.labview', Json.jAny, {}); const [edited, setEdited]: any = React.useState(); const triggerDefault = React.useRef(); const { current, shapes = {} } = local; @@ -571,7 +572,7 @@ function CustomGroup({ function CustomizePanel( { dnd, settings, library, shape, setShape, setDragging }: any, ) { - Dome.useUpdate('labview.library'); + Dome.useUpdate(Library.update); const { items } = library; const views = getItems(items, 'views'); const groups = getItems(items, 'groups'); @@ -640,9 +641,9 @@ export function LabView(props: any) { const settingPanel = settings && `${settings}.panel`; // Hooks & State Dome.useUpdate( - 'labview.library', - 'dome.settings.window', - 'dome.settings.global', + Library.update, + Dome.windowSettings, + Dome.globalSettings, ); const dnd = React.useMemo(() => new DnD(), []); const lib = React.useMemo(() => new Library(), []); diff --git a/ivette/src/frama-c/server.ts b/ivette/src/frama-c/server.ts index 51190f03fdc8debdf08ab53104736a44a02d1a76..33591e19bacfb0e8656c34cc45cede650c7d1525 100644 --- a/ivette/src/frama-c/server.ts +++ b/ivette/src/frama-c/server.ts @@ -32,7 +32,7 @@ const D = new Dome.Debug('Server'); * This event is emitted whenever the server status changes. */ -const STATUS = 'frama-c.server.status'; +const STATUS = new Dome.Event<Status>('frama-c.server.status'); /** * Server is actually started and running. @@ -40,7 +40,7 @@ const STATUS = 'frama-c.server.status'; * This event is emitted when ther server _enters_ the `ON` state. * The server is now ready to handle requests. */ -const READY = 'frama-c.server.ready'; +const READY = new Dome.Event('frama-c.server.ready'); /** * Server Status Notification Event @@ -48,21 +48,18 @@ const READY = 'frama-c.server.ready'; * This event is emitted when ther server _leaves_ the `ON` state. * The server is no more able to handle requests until restart. */ -const SHUTDOWN = 'frama-c.server.shutdown'; +const SHUTDOWN = new Dome.Event('frama-c.server.shutdown'); /** - * Server Signal Prefix + * Server Signal event constructor. * Event `frama-c.server.signal.<id>'` for signal `<id>`. */ -const SIGNAL = 'frama-c.server.signal.'; - -/** - * Server Signal Activity Prefix - - * Event `frama-c.server.activity.<id>'` for signal `<id>`. - */ -const ACTIVITY = 'frama-c.server.activity.'; +export class SIGNAL extends Dome.Event { + constructor(signal: string) { + super(`frama-c.server.signal.${signal}`); + } +} // -------------------------------------------------------------------------- // --- Server Status @@ -200,22 +197,13 @@ export function getPending(): number { * Register callback on `READY` event. * @param {function} callback Invoked when the server enters [[ON]] stage. */ -export function onReady(callback: any) { Dome.on(READY, callback); } +export function onReady(callback: () => void) { READY.on(callback); } /** * Register callback on `SHUTDOWN` event. * @param {function} callback Invoked when the server leaves [[ON]] stage. */ -export function onShutdown(callback: any) { Dome.on(SHUTDOWN, callback); } - -/** - * Register callback on a signal `ACTIVITY` event. - * @param {string} id The signal identifier to listen to. - * @param {function} callback Invoked with `callback(signal, active)`. - */ -export function onActivity(signal: string, callback: any) { - Dome.on(ACTIVITY + signal, callback); -} +export function onShutdown(callback: () => void) { SHUTDOWN.on(callback); } // -------------------------------------------------------------------------- // --- Status Update @@ -229,9 +217,9 @@ function _status(newStatus: Status) { if (newStatus !== status) { const oldStatus = status; status = newStatus; - Dome.emit(STATUS); - if (oldStatus.stage === Stage.ON) Dome.emit(SHUTDOWN); - if (newStatus.stage === Stage.ON) Dome.emit(READY); + STATUS.emit(newStatus); + if (oldStatus.stage === Stage.ON) SHUTDOWN.emit(); + if (newStatus.stage === Stage.ON) READY.emit(); } } @@ -368,12 +356,9 @@ export function restart() { export function clear() { switch (status.stage) { case Stage.FAILURE: - buffer.clear(); - _status(okStatus(Stage.OFF)); - return; case Stage.OFF: buffer.clear(); - Dome.emit(STATUS); + _status(okStatus(Stage.OFF)); return; default: return; @@ -587,7 +572,7 @@ function _exit(error?: Error) { class SignalHandler { id: any; - event: string; + event: Dome.Event; active: boolean; listen: boolean; @@ -601,18 +586,20 @@ class SignalHandler { this.unplug = this.unplug.bind(this); } - on(callback: any) { - const n = System.emitter.listenerCount(this.event); - Dome.on(this.event, callback); + on(callback: () => void) { + const e = this.event; + const n = e.listenerCount(); + e.on(callback); if (n === 0) { this.active = true; if (isRunning()) this.sigon(); } } - off(callback: any) { - Dome.off(this.event, callback); - const n = System.emitter.listenerCount(this.event); + off(callback: () => void) { + const e = this.event; + e.off(callback); + const n = e.listenerCount(); if (n === 0) { this.active = false; if (isRunning()) this.sigoff(); @@ -622,7 +609,6 @@ class SignalHandler { /* Bound to this */ sigon() { if (this.active && !this.listen) { - Dome.emit(ACTIVITY + this.id, true); this.listen = true; queueCmd.push('SIGON', this.id); _flush(); @@ -632,7 +618,6 @@ class SignalHandler { /* Bound to this, Debounced */ sigoff() { if (!this.active && this.listen) { - Dome.emit(ACTIVITY + this.id, false); if (isRunning()) { this.listen = false; queueCmd.push('SIGOFF', this.id); @@ -699,13 +684,13 @@ export function useSignal(s: Signal, callback: any) { // --- Server Synchro -Dome.on(READY, () => { +READY.on(() => { signals.forEach((h: SignalHandler) => { h.sigon(); }); }); -Dome.on(SHUTDOWN, () => { +SHUTDOWN.on(() => { signals.forEach((h: SignalHandler) => { h.unplug(); (h.sigoff as unknown as _.Cancelable).cancel(); @@ -854,7 +839,7 @@ async function _send() { // No pending command nor pending response rqCount = 0; } - Dome.emit(STATUS); + STATUS.emit(status); } } @@ -891,7 +876,7 @@ function _receive(resp: any) { break; case 'SIGNAL': rid = shift(); - Dome.emit(SIGNAL + rid); + (new SIGNAL(rid)).emit(); break; case 'WRONG': err = shift(); diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index edad69f58074965555c2f6984d374ebefb3eb722..b37f0fd7d97e0aab0d47bb80d029adcac818b50e 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -18,8 +18,12 @@ import { useModel } from 'dome/table/models'; import { CompactModel } from 'dome/table/arrays'; import * as Server from './server'; -const PROJECT = 'frama-c.project'; -const STATE_PREFIX = 'frama-c.state.'; +const PROJECT = new Dome.Event('frama-c.project'); +class STATE extends Dome.Event { + constructor(id: string) { + super(`frama-c.state.${id}`); + } +} // -------------------------------------------------------------------------- // --- Pretty Printing (Browser Console) @@ -43,7 +47,7 @@ Server.onReady(async () => { }; const current: { id?: string } = await Server.send(sr, null); currentProject = current.id; - Dome.emit(PROJECT); + PROJECT.emit(); } catch (error) { D.error(`Fail to retrieve the current project. ${error.toString()}`); } @@ -51,7 +55,7 @@ Server.onReady(async () => { Server.onShutdown(() => { currentProject = ''; - Dome.emit(PROJECT); + PROJECT.emit(); }); // -------------------------------------------------------------------------- @@ -83,7 +87,7 @@ export async function setProject(project: string) { }; await Server.send(sr, project); currentProject = project; - Dome.emit(PROJECT); + PROJECT.emit(); } catch (error) { D.error(`Fail to set the current project. ${error.toString()}`); } @@ -219,20 +223,20 @@ interface Handler<A> { // shared for all projects class SyncState<A> { - UPDATE: string; + UPDATE: Dome.Event; handler: Handler<A>; upToDate: boolean; value?: A; constructor(h: Handler<A>) { this.handler = h; - this.UPDATE = STATE_PREFIX + h.name; + this.UPDATE = new STATE(h.name); this.upToDate = false; this.value = undefined; this.update = this.update.bind(this); this.getValue = this.getValue.bind(this); this.setValue = this.setValue.bind(this); - Dome.on(PROJECT, this.update); + PROJECT.on(this.update); } getValue() { @@ -248,7 +252,7 @@ class SyncState<A> { this.value = v; const setter = this.handler.getter; if (setter) await Server.send(setter, v); - Dome.emit(this.UPDATE); + this.UPDATE.emit(); } catch (error) { D.error( `Fail to set value of syncState '${this.handler.name}'.`, @@ -262,7 +266,7 @@ class SyncState<A> { this.upToDate = true; const v = await Server.send(this.handler.getter, null); this.value = v; - Dome.emit(this.UPDATE); + this.UPDATE.emit(); } catch (error) { D.error( `Fail to update syncState '${this.handler.name}'.`, diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx index 6ad7fc3b0222fcecb61b8cd6118d489a57ba1aad..853f8b06ccd78a2ffc1298cd05564707868228cb 100644 --- a/ivette/src/renderer/Controller.tsx +++ b/ivette/src/renderer/Controller.tsx @@ -98,7 +98,7 @@ function insertConfig(hs: string[], cfg: Server.Configuration) { let reloadCommand: string | undefined; -Dome.onReload(() => { +Dome.reload.on(() => { const [lastCmd] = Settings.getWindowSettings( 'Controller.history', Json.jList(Json.jString), [], ); @@ -179,10 +179,14 @@ const RenderConsole = () => { 'Controller.history', Json.jList(Json.jString), [], ); - Dome.useEmitter(editor, 'change', () => { - const cmd = editor.getValue().trim(); - setEmpty(cmd === ''); - setNoTrash(cursor === 0 && history.length === 1 && cmd === history[0]); + React.useEffect(() => { + const callback = () => { + const cmd = editor.getValue().trim(); + setEmpty(cmd === ''); + setNoTrash(noTrash && cmd === history[0]); + }; + editor.on('change', callback); + return () => { editor.off('change', callback); }; }); const doReload = () => {