diff --git a/ivette/src/dome/renderer/controls/icons.tsx b/ivette/src/dome/renderer/controls/icons.tsx index 77a35a1b31bb8fe0b5f3804fe51dcc6daa662e7a..7c7fdde6ef9da3aae74351673472cdb51085e43b 100644 --- a/ivette/src/dome/renderer/controls/icons.tsx +++ b/ivette/src/dome/renderer/controls/icons.tsx @@ -59,6 +59,8 @@ export interface SVGprops { Default is set to `-0.125` times the size). */ offset?: number; + className?: string; + fill?: string; } /** @@ -66,7 +68,7 @@ export interface SVGprops { Only returns the identified `<svg/>` element from Icon base. */ export function SVG(props: SVGprops): null | JSX.Element { - const { id } = props; + const { id, className, fill } = props; if (!id) return null; const icon = Icons[id]; if (!icon) return <>{id}</>; @@ -82,9 +84,10 @@ export function SVG(props: SVGprops): null | JSX.Element { width={size} style={{ bottom: offset }} viewBox={viewBox} + className = {className} > {title && <title>{title}</title>} - <path d={path} /> + <path d={path} fill={fill} /> </svg> ); } diff --git a/ivette/src/dome/renderer/dark.css b/ivette/src/dome/renderer/dark.css index ee7b62fbc147e752a86bd4e70e32e0049a54db57..265d1d7d23c681419dac7002fb30920581eabc86 100644 --- a/ivette/src/dome/renderer/dark.css +++ b/ivette/src/dome/renderer/dark.css @@ -45,6 +45,7 @@ --default-button-hover: #364355; --default-button-active: #303d4f; + --primary-button-color: #146bbf; --primary-button-img: linear-gradient(to bottom, #146bbf 0%, #1960b2 100%); --primary-button-hover: #0e65b9; --primary-button-active: #085fb3; diff --git a/ivette/src/dome/renderer/frame/style.css b/ivette/src/dome/renderer/frame/style.css index 266bed0e505b00875e050e9834c43bf5ada4b9d7..f9d783ae1212313089d556f9bc563263e9a68f22 100644 --- a/ivette/src/dome/renderer/frame/style.css +++ b/ivette/src/dome/renderer/frame/style.css @@ -430,68 +430,98 @@ } /* -------------------------------------------------------------------------- */ -/* --- Styling ToolBar Search Field --- */ +/* --- Styling ToolBar Action Field --- */ /* -------------------------------------------------------------------------- */ -.dome-xToolBar-control.dome-xToolBar-searchfield { - background-image: none ; - background-color: var(--background-alterning-odd); - padding-left: 20px ; - border-radius: 12px ; - border-color: var(--border); - color: var(--text); - width: 32px ; - transition: width 0.4s ease-in-out ; +.dome-xToolBar-actionComponent { + overflow: visible; } -.dome-xToolBar-searchfield:focus, -.dome-xToolBar-searchfield:hover, -.dome-xToolBar-searchicon:hover + .dome-xToolBar-searchfield -{ - width: 160px; +.dome-xToolBar-actionField { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + margin: 2px 1px 2px 1px; + padding: 0; + overflow: hidden; + font-size: 13px; + width: 28px; + border-radius: 12px; + transition: width 0.4s ease-in-out; + z-Index: +1; } -.dome-xToolBar-searchicon { - position: relative ; - overflow: visible ; - z-Index: +1; - height: 0px; - width: 0px; - top: -7px ; - left: 12px ; +.dome-xToolBar-actionField:hover { + width: 228px; } -.dome-xToolBar-searchmenu { - position: relative ; - width: 162px ; - max-height: 120px ; - overflow-x: hidden ; - overflow-y: auto ; - left: -7px ; - top: 4px ; - padding: 0px ; - border: 1pt solid var(--border) ; - background: var(--background-alterning-odd) ; +.dome-xToolBar-actionField:focus-within { + width: 228px !important; + box-shadow: 0 0 2px var(--border-discrete); +} + +.dome-xToolBar-actionField div.dome-xToolBar-modeSelection { + position: relative; + padding: 3px 8px 0px 8px; + cursor: pointer; +} + +.dome-xToolBar-actionField SVG.dome-xToolBar-modeIcon { + position: relative; +} + +/* Style the search box inside the navigation bar */ +.dome-xToolBar-actionField input[type=search] { + border: none; + border-top-right-radius: 12px; + border-bottom-right-radius: 12px; + text-indent: 4px; + height: 21px; + width: 200px; + outline: none; + background: var(--background-softer); +} + +.dome-xToolBar-actionComponent div.dome-xToolBar-suggestions { + position: absolute; + overflow-x: hidden; + overflow-y: auto; + max-height: 120px; + z-index: +1; + width: 0px; + background: var(--background-alterning-odd); + border-radius: 12px; +} + +.dome-xToolBar-actionComponent:focus-within div.dome-xToolBar-suggestions { + width: 230px; } .dome-xToolBar-searchitem { - color: var(--text); - display: block ; - width: 100% ; - margin-left: 0px ; - margin-right: 0px ; - padding-left: 4px ; - padding-right: 2px ; + display: block ; + width: 100% ; + margin-left: 0px ; + margin-right: 0px ; + padding-left: 4px ; + padding-right: 2px ; } -.dome-xToolBar-searchitem:hover, -.dome-xToolBar-searchindex -{ - background: var(--selected-element) !important; +.dome-xToolBar-searchitem:hover, .dome-xToolBar-searchindex { + background: var(--selected-element) !important; } .dome-xToolBar-searchitem:nth-child(even) { - background: var(--background-alterning-even); + background: var(--background-alterning-even); +} + +.dome-xToolBar-searchMode { + background: var(--primary-button-color); + fill: var(--text-discrete); +} + +.dome-xToolBar-modeOfModes { + background: var(--background-profound); + fill: var(--text-discrete); } /* -------------------------------------------------------------------------- */ diff --git a/ivette/src/dome/renderer/frame/toolbars.tsx b/ivette/src/dome/renderer/frame/toolbars.tsx index 51ab7f1a51a34b6623b7188a55f2d270c6132eef..ab724b5e32edb3476d77d3ff66cb9bcc7331d5f1 100644 --- a/ivette/src/dome/renderer/frame/toolbars.tsx +++ b/ivette/src/dome/renderer/frame/toolbars.tsx @@ -32,7 +32,7 @@ */ import React from 'react'; -import { Event, useEvent, find } from 'dome'; +import { Event, find, usePromise } from 'dome'; import { SVG } from 'dome/controls/icons'; import { Label } from 'dome/controls/labels'; import { classes } from 'dome/misc/utils'; @@ -265,92 +265,190 @@ export function Select(props: SelectionProps<string>) { } // -------------------------------------------------------------------------- -// --- SearchField +// --- ModalActionField necessary types // -------------------------------------------------------------------------- -const DEBOUNCED_SEARCH = 200; - -const scrollToRef = (r: null | HTMLLabelElement) => { - if (r) r.scrollIntoView({ block: 'nearest' }); -}; - -export interface Hint<A> { +/** Description of a hint used to populate the suggestions. */ +export interface Hint { id: string | number; icon?: string; label: string | JSX.Element; title?: string; - value: A; + value(): void; + rank?: number; } -export interface SearchFieldProps<A> { - /** Tooltip Text. */ - title?: string; - /** Placeholder Text. */ +/** Total order on hints. */ +export function byHint(a: Hint, b: Hint) { + const ra = a.rank ?? 0; + const rb = b.rank ?? 0; + if (ra < rb) return -1; + if (ra > rb) return +1; + return 0; +} + +/** Type alias for functions that build hints list from a pattern. */ +export type HintsEvaluator = (pattern: string) => Promise<Hint[]> + +/** Description of an action mode. */ +export interface ActionMode { + /** Mode tooltip text. */ + title: string; + /** Mode placeholder text. */ placeholder?: string; - /** Provided search hints (with respect to last `onSearch()` callback). */ - hints?: Hint<A>[]; - /** Search callback. Triggered on Enter Key, Escape Key or Blur event. */ - onSelect?: (pattern: string) => void; - /** Dynamic search callback. Triggered on key pressed (debounced). */ - onSearch?: (pattern: string) => void; - /** Hint selection callback. */ - onHint?: (hint: Hint<A>) => void; - /** Event that triggers a focus request (defaults to [[dome.find]]). */ - event?: null | Event<void>; + /** Icon displayed when the mode is selected. */ + icon: string; + /** CSS class for the mode section. */ + className: string; + /** Hints provider. */ + hints: HintsEvaluator; + /** Hint selection callback. Defaults to evaluate the <value> field. */ + onHint?: (hint: Hint) => void; + /** Action to perform when Enter is hit. Useful for modes without hints. */ + onEnter?: (pattern: string) => void; + /** Event that triggers a focus request. */ + event?: Event<void>; } -interface Searching { - pattern?: string; - timer?: NodeJS.Timeout | undefined; - onSearch?: ((p: string) => void); +// -------------------------------------------------------------------------- +// --- ModalActionField default mode: Search +// -------------------------------------------------------------------------- + +const searchEnginesIds = new Set<string>(); +let searchEvaluators: HintsEvaluator[] = [] + +export function registerSearchHints(id: string, search: HintsEvaluator) { + if (!(id in searchEnginesIds)) { + searchEnginesIds.add(id); + searchEvaluators = searchEvaluators.concat(search); + } } -/** - Search Bar. - */ -export function SearchField<A = undefined>(props: SearchFieldProps<A>) { - const inputRef = React.useRef<HTMLInputElement | null>(null); - const blur = () => inputRef.current?.blur(); - const focus = () => inputRef.current?.focus(); - const [value, setValue] = React.useState(''); - const [index, setIndex] = React.useState(-1); - const searching = React.useRef<Searching>({}); - const { onHint, onSelect, onSearch, hints = [] } = props; - - // Find event trigger - useEvent(props.event ?? find, focus); - - // Lookup trigger - const triggerLookup = React.useCallback((pattern: string) => { - const s = searching.current; - s.pattern = pattern; - s.onSearch = onSearch; - if (!s.timer) { - s.timer = setTimeout(() => { - s.timer = undefined; - if (s.onSearch && s.pattern) s.onSearch(s.pattern); - }, DEBOUNCED_SEARCH); - } - }, [onSearch]); +async function searchModeHints(pattern: string) { + if (pattern === '') return []; + const promises = searchEvaluators.map((E: HintsEvaluator) => E(pattern)); + const hints = await Promise.all(promises); + return hints.flat().sort(byHint); +} + +const searchMode: ActionMode = { + title: "Search", + placeholder: "Search…", + icon: "SEARCH", + className: 'dome-xToolBar-searchMode', + hints: searchModeHints, + event: find, +} + +// -------------------------------------------------------------------------- +// --- ModalActionField mode button component +// -------------------------------------------------------------------------- + +interface ModeButtonComponentProps { + current: ActionMode; + onClick: () => void; +} + +function ModeButton(props: ModeButtonComponentProps) { + const { current, onClick } = props; + return ( + <div + className={classes("dome-xToolBar-modeSelection", current.className)} + onClick={onClick} + > + <SVG + className="dome-xToolBar-modeIcon" + id={current.icon} + offset={-1} + /> + </div> + ); +} + +// -------------------------------------------------------------------------- +// --- ModalActionField suggestions component +// -------------------------------------------------------------------------- + +interface SuggestionsProps { + hints: Hint[]; + onHint: (hint: Hint) => void; + index: number; +} + +function scrollToRef (r: null | HTMLLabelElement) { + if (r) r.scrollIntoView({ block: 'nearest' }); +} + +function Suggestions(props: SuggestionsProps) { + const { hints, onHint, index } = props; + + // Computing the relevant suggestions. */ + const suggestions = hints.map((h, k) => { + const selected = k === index || hints.length === 1; + const classSelected = selected && 'dome-xToolBar-searchindex'; + const className = classes('dome-xToolBar-searchitem', classSelected); + return ( + <Label + ref={selected ? scrollToRef : undefined} + key={h.id} + icon={h.icon} + title={h.title} + className={className} + onClick={() => onHint(h)} + > + {h.label} + </Label> + ); + }); + + // Rendering the component. + return ( + <div + style={{ visibility: suggestions.length > 0 ? 'visible' : 'hidden' }} + className='dome-xToolBar-suggestions' + onMouseDown={ (event) => event.preventDefault() } + > + {suggestions} + </div> + ); +} + +// -------------------------------------------------------------------------- +// --- ModalActionField input field component +// -------------------------------------------------------------------------- + +interface ActionInputProps { + title: string; + placeholder?: string; + hints: Hint[]; + onHint: (hint: Hint) => void; + onEnter?: (pattern: string) => void; + index: number; + setIndex: (n: number) => void; + pattern: string; + setPattern: (p: string) => void; + inputRef: React.MutableRefObject<HTMLInputElement | null>; +} + +function ActionInput(props: ActionInputProps) { + const { title, placeholder, hints, onHint, onEnter } = props; + const { index, setIndex, pattern, setPattern, inputRef } = props; // Blur Event - const onBlur = () => { - setValue(''); - setIndex(-1); - if (onSearch) onSearch(''); - }; + const onBlur = () => { setPattern(''); setIndex(-1); }; - // Key Events - const onKey = (evt: React.KeyboardEvent) => { + // Key Up Events + const onKeyUp = (evt: React.KeyboardEvent) => { + const blur = () => inputRef.current?.blur(); switch (evt.key) { case 'Escape': blur(); break; case 'Enter': - if (index >= 0 && index < hints.length) { - if (onHint) onHint(hints[index]); - } else if (onSelect) onSelect(value); blur(); + if (index >= 0 && index < hints.length) onHint(hints[index]); + else if (hints.length === 1) onHint(hints[0]); + else if (onEnter) onEnter(pattern); break; case 'ArrowUp': if (index < 0) setIndex(hints.length - 1); @@ -363,67 +461,136 @@ export function SearchField<A = undefined>(props: SearchFieldProps<A>) { } }; - // Input Events + // Key Down Events. Disables the default behavior on ArrowUp and ArrowDown. + const onKeyDown = (evt: React.KeyboardEvent) => { + if (evt.key === 'ArrowUp' || evt.key === 'ArrowDown') evt.preventDefault(); + }; + + // // Input Events const onChange = (evt: React.ChangeEvent<HTMLInputElement>) => { - const newValue = evt.target.value; - triggerLookup(newValue); setIndex(-1); - setValue(newValue); + setPattern(evt.target.value); }; - // Render Suggestions - const suggestions = hints.map((h, k) => { - const selected = k === index || hints.length === 1; - const className = classes( - 'dome-xToolBar-searchitem', - selected && 'dome-xToolBar-searchindex', - ); - return ( - <Label - ref={selected ? scrollToRef : undefined} - key={h.id} - icon={h.icon} - title={h.title} - className={className} - onClick={() => { - if (onHint) onHint(h); - blur(); - }} - > - {h.label} - </Label> - ); + return ( + <input + type="search" + placeholder={placeholder} + ref={inputRef} + title={title} + value={pattern} + onKeyUp={onKeyUp} + onKeyDown={onKeyDown} + onChange={onChange} + onBlur={onBlur} + /> + ); +} + +// -------------------------------------------------------------------------- +// --- ModalActionField component +// -------------------------------------------------------------------------- + +export const RegisterMode: Event<ActionMode> = + new Event('dome.actionmode.register'); + +export const UnregisterMode: Event<ActionMode> = + new Event('dome.actionmode.unregister'); + +export function ModalActionField() { + + // Internal state of the component along with useful functions acting on it. + const inputRef = React.useRef<HTMLInputElement | null>(null); + const [index, setIndex] = React.useState(-1); + const [pattern, setPattern] = React.useState(''); + const [current, onModeChange] = React.useState<ActionMode>(searchMode); + const focus = () => inputRef.current?.focus(); + const changeMode = (m: ActionMode) => () => { onModeChange(m); focus(); }; + const toDefault = () => onModeChange(searchMode); + const reset = (m: ActionMode) => { if (current === m) toDefault(); }; + + // Set of all modes currently active. We populate it by reacting to + // RegisterMode and UnregisterMode events. We also activate the mode event if + // available. Everything is cleaned when the component is unmounted. + const [allModes] = React.useState<Set<ActionMode>>(new Set()); + React.useEffect(() => { + const on = (m: ActionMode) => m.event?.on(changeMode(m)); + const register = (m: ActionMode) => { allModes.add(m); on(m) }; + const off = (m: ActionMode) => m.event?.off(changeMode(m)); + const remove = (m: ActionMode) => { allModes.delete(m); off(m); reset(m); }; + RegisterMode.on(register); UnregisterMode.on(remove); + return () => { RegisterMode.off(register); UnregisterMode.off(remove); }; }); - const haspopup = - inputRef.current === document.activeElement - && suggestions.length > 0; - const visibility = haspopup ? 'visible' : 'hidden'; - // Render Component + // Register the search mode. + React.useEffect(() => RegisterMode.emit(searchMode)); + + // Compute the hints for the current mode. + const { hints, onHint = (h) => h.value(), onEnter } = current; + const hintsPromise = React.useMemo(() => hints(pattern), [pattern, hints]); + const { result = [] } = usePromise(hintsPromise); + + // Auxiliary function that build a Hint from an ActionMode. + const modeToHint = (mode: ActionMode) => { + const { title, icon } = mode; + const id = "ActionMode-"+ title + "-" + icon; + const value = () => { onModeChange(mode); }; + return { id, icon, label: title, title, value, rank: -1000 }; + }; + + // Hints provider for the mode of all modes. + const modesHints = React.useCallback((pattern: string) => { + const p = pattern.toLowerCase(); + const fit = (m: ActionMode) => m.title.toLowerCase().includes(p); + return Promise.resolve(Array.from(allModes).filter(fit).map(modeToHint)); + }, [allModes]); + + // Build the mode of all modes. This special mode is activated when clicking + // on the current mode icon and allows to change the current mode, displaying + // a list of all available modes as hints. + const modesMode = React.useMemo(() => { + const title = "Mode selection"; + const placeholder = "Search mode"; + const icon = "TUNINGS"; + const className = 'dome-xToolBar-modeOfModes' + return { title, placeholder, icon, className, hints: modesHints }; + }, []); + + // Build a new search engine for the search mode, adding available modes to + // the possible search hints. + const searchModeHints = async (pattern: string) => { + const hints = await modesMode.hints(pattern); + const notCurrent = (h: Hint) => !(h.title?.includes(current.title)); + return hints.filter(notCurrent); + }; + + // Register the new search engine. + const register = () => registerSearchHints("ModesMode", searchModeHints); + React.useEffect(register, [searchMode, modesMode]); + + // Build the component. + const { title, placeholder } = current; + const handleModeClick = changeMode(modesMode); + const onBlur = () => reset(modesMode); return ( - <> - <div className="dome-xToolBar-searchicon"> - <SVG id="SEARCH" /> - <div - style={{ visibility }} - className="dome-xToolBar-searchmenu" - onMouseDown={(event) => event.preventDefault()} - > - {suggestions} - </div> + <div className="dome-xToolBar-actionComponent" onBlur={onBlur}> + <div className="dome-xToolBar-actionField"> + <ModeButton current={current} onClick={handleModeClick} /> + <ActionInput + title={title} + placeholder={placeholder} + hints={result} + onHint={onHint} + onEnter={onEnter} + index={index} + setIndex={setIndex} + pattern={pattern} + setPattern={setPattern} + inputRef={inputRef} + /> </div> - <input - ref={inputRef} - type="search" - title={props.title} - value={value} - placeholder={props.placeholder} - className="dome-xToolBar-control dome-xToolBar-searchfield" - onKeyUp={onKey} - onChange={onChange} - onBlur={onBlur} - /> - </> + <Suggestions hints={result} onHint={onHint} index={index} /> + </div> ); } diff --git a/ivette/src/dome/renderer/light.css b/ivette/src/dome/renderer/light.css index e70918a90f1e1f35a7b2ac7be6a7cbce40a9e394..1eb31da53fc00624364f9f6577ef1114ef196c60 100644 --- a/ivette/src/dome/renderer/light.css +++ b/ivette/src/dome/renderer/light.css @@ -45,6 +45,7 @@ --default-button-hover: #ffffff; --default-button-active: #f9f9f9; + --primary-button-color: #449bef; --primary-button-img: linear-gradient(to bottom, #449bef 0%, #4990e2 100%); --primary-button-hover: #00b6ff; --primary-button-active: #ddd; diff --git a/ivette/src/frama-c/kernel/Globals.tsx b/ivette/src/frama-c/kernel/Globals.tsx index de8ef1cabd5c4f9c7d89c0d5c80fef5a340c64a1..57adbb92980d9b5ad2a9b7c0fcee2b30c57bb7fe 100644 --- a/ivette/src/frama-c/kernel/Globals.tsx +++ b/ivette/src/frama-c/kernel/Globals.tsx @@ -30,7 +30,7 @@ import { classes } from 'dome/misc/utils'; import { alpha } from 'dome/data/compare'; import { Section, Item } from 'dome/frame/sidebars'; import { Button } from 'dome/controls/buttons'; -import * as Ivette from 'ivette'; +import * as Toolbars from 'dome/frame/toolbars'; import * as States from 'frama-c/states'; import { functions, functionsData } from 'frama-c/kernel/api/ast'; @@ -40,16 +40,16 @@ import { computationState } from 'frama-c/plugins/eva/api/general'; // --- Global Search Hints // -------------------------------------------------------------------------- -function makeFunctionHint(fct: functionsData): Ivette.Hint { +function makeFunctionHint(fct: functionsData): Toolbars.Hint { return { id: fct.key, label: fct.name, title: fct.signature, - onSelection: () => States.setSelection({ fct: fct.name }), + value: () => States.setSelection({ fct: fct.name }), }; } -async function lookupGlobals(pattern: string): Promise<Ivette.Hint[]> { +async function lookupGlobals(pattern: string): Promise<Toolbars.Hint[]> { const lookup = pattern.toLowerCase(); const fcts = States.getSyncArray(functions).getArray(); return fcts.filter((fn) => ( @@ -57,7 +57,7 @@ async function lookupGlobals(pattern: string): Promise<Ivette.Hint[]> { )).map(makeFunctionHint); } -Ivette.registerHints('frama-c.globals', lookupGlobals); +Toolbars.registerSearchHints('frama-c.globals', lookupGlobals); // -------------------------------------------------------------------------- // --- Function Item diff --git a/ivette/src/ivette/index.tsx b/ivette/src/ivette/index.tsx index 4cd9fa9f83bb89ad26b124c72c68d3aa8b4f9b36..596110753d9bd49d9f689ad019cc86a216138fe1 100644 --- a/ivette/src/ivette/index.tsx +++ b/ivette/src/ivette/index.tsx @@ -180,32 +180,6 @@ export function TitleBar(props: TitleBarProps) { ); } -/* --------------------------------------------------------------------------*/ -/* --- Search Hints ---*/ -/* --------------------------------------------------------------------------*/ - -export interface Hint { - id: string; - label: string | JSX.Element; - title?: string; - rank?: number; - onSelection: () => void; -} - -/** - Register a hint search engine for the Ivette toolbar. -*/ -export function registerHints( - id: string, - lookup: (pattern: string) => Promise<Hint[]>, -) { - const adaptor = (h: Hint): Ext.SearchHint => ( - { ...h, value: () => h.onSelection() } - ); - const search = (p: string) => lookup(p).then((hs) => hs.map(adaptor)); - Ext.registerHints({ id, search }); -} - /* --------------------------------------------------------------------------*/ /* --- Sidebar Panels ---*/ /* --------------------------------------------------------------------------*/ diff --git a/ivette/src/renderer/Application.tsx b/ivette/src/renderer/Application.tsx index b3ead33936b44072cc8e0354aaa068020574a3ff..42bfd85a9f252bb9d1ef723bcf49450eadd3d7f0 100644 --- a/ivette/src/renderer/Application.tsx +++ b/ivette/src/renderer/Application.tsx @@ -47,10 +47,7 @@ export default function Application(): JSX.Element { Dome.useFlipSettings('frama-c.sidebar.unfold', true); const [viewbar, flipViewbar] = Dome.useFlipSettings('frama-c.viewbar.unfold', true); - const hints = Extensions.useSearchHints(); - const onSelectedHints = (): void => { - if (hints.length === 1) Extensions.onSearchHint(hints[0]); - }; + return ( <Vfill> <Toolbar.ToolBar> @@ -63,15 +60,9 @@ export default function Application(): JSX.Element { <Controller.Control /> <Extensions.Toolbar /> <Toolbar.Filler /> - <Toolbar.SearchField - placeholder="Search…" - hints={hints} - onSearch={Extensions.searchHints} - onHint={Extensions.onSearchHint} - onSelect={onSelectedHints} - /> <IvettePrefs.ThemeSwitchTool /> <IvettePrefs.FontTools /> + <Toolbar.ModalActionField /> <Toolbar.Button icon="ITEMS.GRID" title="Customize Main View" diff --git a/ivette/src/renderer/Extensions.tsx b/ivette/src/renderer/Extensions.tsx index 2d33b9f1dc520ca6f916bfc61161923db6f73a48..145f9b66b1edf59e1dbe3064a763f7ecd83f95d4 100644 --- a/ivette/src/renderer/Extensions.tsx +++ b/ivette/src/renderer/Extensions.tsx @@ -28,84 +28,6 @@ import React from 'react'; import * as Dome from 'dome'; -import { Hint } from 'dome/frame/toolbars'; - -/* --------------------------------------------------------------------------*/ -/* --- Search Hints ---*/ -/* --------------------------------------------------------------------------*/ - -export interface HintCallback { - (): void; -} - -export interface SearchHint extends Hint<HintCallback> { - rank?: number; -} - -function bySearchHint(a: SearchHint, b: SearchHint) { - const ra = a.rank ?? 0; - const rb = b.rank ?? 0; - if (ra < rb) return -1; - if (ra > rb) return +1; - return 0; -} - -export interface SearchEngine { - id: string; - search: (pattern: string) => Promise<SearchHint[]>; -} - -const NEWHINTS = new Dome.Event('ivette.hints'); -const HINTLOOKUP = new Map<string, SearchEngine>(); -const HINTS = new Map<string, SearchHint[]>(); -let CURRENT = ''; - -export function updateHints() { - if (CURRENT !== '') - NEWHINTS.emit(); -} - -export function registerHints(E: SearchEngine) { - HINTLOOKUP.set(E.id, E); -} - -export function searchHints(pattern: string) { - if (pattern === '') { - CURRENT = ''; - HINTS.clear(); - NEWHINTS.emit(); - } else { - const REF = pattern; - CURRENT = pattern; - HINTLOOKUP.forEach((E: SearchEngine) => { - E.search(REF).then((hs) => { - if (REF === CURRENT) { - HINTS.set(E.id, hs); - NEWHINTS.emit(); - } - }).catch(() => { - if (REF === CURRENT) { - HINTS.delete(E.id); - NEWHINTS.emit(); - } - }); - }); - } -} - -export function onSearchHint(h: SearchHint) { - h.value(); -} - -export function useSearchHints() { - const [hints, setHints] = React.useState<SearchHint[]>([]); - Dome.useEvent(NEWHINTS, () => { - let hs: SearchHint[] = []; - HINTS.forEach((rhs) => { hs = hs.concat(rhs); }); - setHints(hs.sort(bySearchHint)); - }); - return hints; -} /* --------------------------------------------------------------------------*/ /* --- Extension Elements ---*/