Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • pub/frama-c
  • proidiot/frama-c
  • lthls/frama-c
3 results
Show changes
Showing
with 554 additions and 331 deletions
......@@ -45,6 +45,7 @@ import {
BrowserWindowConstructorOptions,
IpcMainEvent,
shell,
dialog,
} from 'electron';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'dome/devtools';
import SYS, * as System from 'dome/system';
......@@ -174,9 +175,9 @@ ipcMain.on('dome.ipc.settings.sync', windowSyncSettings);
// --- Patching Settings
// --------------------------------------------------------------------------
type patch = { key: string; value: any };
type Patch = { key: string; value: any };
function applyPatches(data: Store, args: patch[]) {
function applyPatches(data: Store, args: Patch[]) {
args.forEach(({ key, value }) => {
if (value === null) {
delete data[key];
......@@ -186,7 +187,7 @@ function applyPatches(data: Store, args: patch[]) {
});
}
function applyWindowSettings(event: IpcMainEvent, args: patch[]) {
function applyWindowSettings(event: IpcMainEvent, args: Patch[]) {
const handle = WindowHandles.get(event.sender.id);
if (handle) {
applyPatches(handle.settings, args);
......@@ -194,7 +195,7 @@ function applyWindowSettings(event: IpcMainEvent, args: patch[]) {
}
}
function applyStorageSettings(event: IpcMainEvent, args: patch[]) {
function applyStorageSettings(event: IpcMainEvent, args: Patch[]) {
const handle = WindowHandles.get(event.sender.id);
if (handle) {
applyPatches(handle.storage, args);
......@@ -202,7 +203,7 @@ function applyStorageSettings(event: IpcMainEvent, args: patch[]) {
}
}
function applyGlobalSettings(event: IpcMainEvent, args: patch[]) {
function applyGlobalSettings(event: IpcMainEvent, args: Patch[]) {
applyPatches(obtainGlobalSettings(), args);
BrowserWindow.getAllWindows().forEach((w: BrowserWindow) => {
const contents = w.webContents;
......@@ -274,7 +275,7 @@ function getURL() {
return `file://${__dirname}/index.html`;
}
function navigateURL(sender: Electron.webContents) {
function navigateURL(sender: Electron.WebContents) {
return (event: Electron.Event, url: string) => {
event.preventDefault();
const href = new URL(url);
......@@ -328,6 +329,7 @@ function createBrowserWindow(
backgroundColor: '#f0f0f0',
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
additionalArguments: [browserArguments],
},
...config,
......@@ -521,6 +523,15 @@ function restoreDefaultSettings() {
ipcMain.on('dome.menu.settings', showSettingsWindow);
ipcMain.on('dome.menu.defaults', restoreDefaultSettings);
ipcMain.on('dome.app.paths', (event) => {
event.returnValue = {
'home': app.getPath('home'),
'desktop': app.getPath('desktop'),
'documents': app.getPath('documents'),
'downloads': app.getPath('downloads'),
'temp': app.getPath('temp'),
};
});
// --------------------------------------------------------------------------
// --- Main Application Starter
......@@ -586,3 +597,22 @@ ipcMain.on('dome.ipc.menu.addmenuitem', (_evt, spec) => addMenuItem(spec));
ipcMain.on('dome.ipc.menu.setmenuitem', (_evt, spec) => setMenuItem(spec));
// --------------------------------------------------------------------------
// --- Dialogs Management
// --------------------------------------------------------------------------
ipcMain.handle(
'dome.dialog.showMessageBox',
(_evt, props) => dialog.showMessageBox(props),
);
ipcMain.handle(
'dome.dialog.showOpenDialog',
(_evt, props) => dialog.showOpenDialog(props),
);
ipcMain.handle(
'dome.dialog.showSaveDialog',
(_evt, props) => dialog.showSaveDialog(props),
);
// --------------------------------------------------------------------------
......@@ -24,10 +24,15 @@
// --- Menus & MenuBar Management
// --------------------------------------------------------------------------
/* eslint-disable max-len */
/* eslint-disable @typescript-eslint/camelcase */
import { app, ipcMain, BrowserWindow, Menu, MenuItem, shell } from 'electron';
import {
app,
ipcMain,
BrowserWindow,
Menu,
MenuItem,
shell,
KeyboardEvent,
} from 'electron';
import * as System from 'dome/system';
// --------------------------------------------------------------------------
......@@ -48,16 +53,33 @@ function reloadWindow() {
});
}
function toggleFullScreen(_item: MenuItem, focusedWindow: BrowserWindow) {
function toggleFullScreen(
_item: MenuItem,
focusedWindow: BrowserWindow | undefined,
_evt: KeyboardEvent,
) {
if (focusedWindow)
focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
}
function toggleDevTools(_item: MenuItem, focusedWindow: BrowserWindow) {
function toggleDevTools(
_item: MenuItem,
focusedWindow: BrowserWindow | undefined,
_evt: KeyboardEvent,
) {
if (focusedWindow)
focusedWindow.webContents.toggleDevTools();
}
function userFindInfo(
_item: MenuItem,
focusedWindow: BrowserWindow | undefined,
_evt: KeyboardEvent,
) {
if (focusedWindow)
focusedWindow.webContents.send('dome.ipc.find');
}
// --------------------------------------------------------------------------
// --- Menu Utilities
// --------------------------------------------------------------------------
......@@ -131,9 +153,9 @@ const macosAppMenuItems = (appName: string): MenuSpec => [
// --- File Menu Items (platform dependant)
// --------------------------------------------------------------------------
const fileMenuItems_custom: MenuSpec = [];
const fileMenuItemsCustom: MenuSpec = [];
const fileMenuItems_linux: MenuSpec = [
const fileMenuItemsLinux: MenuSpec = [
{
label: 'Preferences…',
click: () => ipcMain.emit('dome.menu.settings'),
......@@ -154,7 +176,7 @@ const fileMenuItems_linux: MenuSpec = [
// --- Edit Menu Items
// --------------------------------------------------------------------------
const editMenuItems_custom: MenuSpec = [];
const editMenuItemsCustom: MenuSpec = [];
const editMenuItems: MenuSpec = [
{
......@@ -188,11 +210,7 @@ const editMenuItems: MenuSpec = [
{
label: 'Find',
accelerator: 'CmdOrCtrl+F',
click: (
_item: Electron.MenuItem,
window: Electron.BrowserWindow,
_evt: Electron.KeyboardEvent,
) => window.webContents.send('dome.ipc.find'),
click: userFindInfo,
},
];
......@@ -200,7 +218,7 @@ const editMenuItems: MenuSpec = [
// --- View Menu Items
// --------------------------------------------------------------------------
const viewMenuItems_custom: MenuSpec = [];
const viewMenuItemsCustom: MenuSpec = [];
const viewMenuItems = (osx: boolean): MenuSpec => [
{
......@@ -222,7 +240,7 @@ const viewMenuItems = (osx: boolean): MenuSpec => [
// --- Window Menu Items
// --------------------------------------------------------------------------
const windowMenuItems_linux: MenuSpec = [
const windowMenuItemsLinux: MenuSpec = [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
......@@ -241,7 +259,7 @@ const windowMenuItems_linux: MenuSpec = [
},
];
const windowMenuItems_macos: MenuSpec = windowMenuItems_linux.concat([
const windowMenuItemsMacos: MenuSpec = windowMenuItemsLinux.concat([
{
label: 'Bring All to Front',
role: 'front',
......@@ -292,9 +310,9 @@ const customItems = new Map<string, ItemEntry>();
function findMenu(label: string): MenuSpec | undefined {
switch (label) {
case 'File': return fileMenuItems_custom;
case 'Edit': return editMenuItems_custom;
case 'View': return viewMenuItems_custom;
case 'File': return fileMenuItemsCustom;
case 'Edit': return editMenuItemsCustom;
case 'View': return viewMenuItemsCustom;
default: {
const cm = customMenus.find((m) => m.label === label);
return cm && cm.submenu;
......@@ -317,12 +335,12 @@ export interface CustomMenuItem extends MenuItemSpec {
key?: string;
}
export interface Separator {
export interface SeparatorItem {
menu: string;
type: 'separator';
}
export type CustomMenuItemSpec = Separator | CustomMenuItem;
export type CustomMenuItemSpec = SeparatorItem | CustomMenuItem;
export function addMenuItem(custom: CustomMenuItemSpec) {
const menuSpec = findMenu(custom.menu);
......@@ -337,16 +355,22 @@ export function addMenuItem(custom: CustomMenuItemSpec) {
if (key) {
switch (System.platform) {
case 'macos':
if (key.startsWith('Cmd+')) spec.accelerator = `Cmd+${key.substring(4)}`;
if (key.startsWith('Alt+')) spec.accelerator = `Cmd+Alt+${key.substring(4)}`;
if (key.startsWith('Meta+')) spec.accelerator = `Cmd+Shift+${key.substring(5)}`;
if (key.startsWith('Cmd+'))
spec.accelerator = `Cmd+${key.substring(4)}`;
if (key.startsWith('Alt+'))
spec.accelerator = `Cmd+Alt+${key.substring(4)}`;
if (key.startsWith('Meta+'))
spec.accelerator = `Cmd+Shift+${key.substring(5)}`;
break;
case 'windows':
case 'linux':
default:
if (key.startsWith('Cmd+')) spec.accelerator = `Ctrl+${key.substring(4)}`;
if (key.startsWith('Alt+')) spec.accelerator = `Alt+${key.substring(4)}`;
if (key.startsWith('Meta+')) spec.accelerator = `Ctrl+Alt+${key.substring(5)}`;
if (key.startsWith('Cmd+'))
spec.accelerator = `Ctrl+${key.substring(4)}`;
if (key.startsWith('Alt+'))
spec.accelerator = `Alt+${key.substring(4)}`;
if (key.startsWith('Meta+'))
spec.accelerator = `Ctrl+Alt+${key.substring(5)}`;
break;
}
}
......@@ -361,10 +385,10 @@ export function addMenuItem(custom: CustomMenuItemSpec) {
} else {
if (!spec.click && !spec.role)
spec.click = (
_item: Electron.MenuItem,
window: Electron.BrowserWindow,
_evt: Electron.KeyboardEvent,
) => window.webContents.send('dome.ipc.menu.clicked', id);
_item: MenuItem,
window: BrowserWindow | undefined,
_evt: KeyboardEvent,
) => window?.webContents.send('dome.ipc.menu.clicked', id);
customItems.set(id, { spec });
menuSpec.push(spec);
}
......@@ -392,14 +416,31 @@ function template(): CustomMenu[] {
return ([] as CustomMenu[]).concat(
[
{ label: app.name, submenu: macosAppMenuItems(app.name) },
{ label: 'File', submenu: fileMenuItems_custom },
{ label: 'Edit', submenu: concatSep(editMenuItems, editMenuItems_custom) },
{ label: 'View', submenu: concatSep(viewMenuItems_custom, viewMenuItems(true)) },
{
label: 'File',
submenu: fileMenuItemsCustom,
},
{
label: 'Edit',
submenu: concatSep(editMenuItems, editMenuItemsCustom),
},
{
label: 'View',
submenu: concatSep(viewMenuItemsCustom, viewMenuItems(true)),
},
],
customMenus,
[
{ label: 'Window', role: 'window', submenu: windowMenuItems_macos },
{ label: 'Help', role: 'help', submenu: helpMenuItems },
{
label: 'Window',
role: 'window',
submenu: windowMenuItemsMacos,
},
{
label: 'Help',
role: 'help',
submenu: helpMenuItems,
},
],
);
case 'windows':
......@@ -407,13 +448,22 @@ function template(): CustomMenu[] {
default:
return ([] as CustomMenu[]).concat(
[
{ label: 'File', submenu: concatSep(fileMenuItems_custom, fileMenuItems_linux) },
{ label: 'Edit', submenu: concatSep(editMenuItems, editMenuItems_custom) },
{ label: 'View', submenu: concatSep(viewMenuItems_custom, viewMenuItems(false)) },
{
label: 'File',
submenu: concatSep(fileMenuItemsCustom, fileMenuItemsLinux),
},
{
label: 'Edit',
submenu: concatSep(editMenuItems, editMenuItemsCustom),
},
{
label: 'View',
submenu: concatSep(viewMenuItemsCustom, viewMenuItems(false)),
},
],
customMenus,
[
{ label: 'Window', submenu: windowMenuItems_linux },
{ label: 'Window', submenu: windowMenuItemsLinux },
{ label: 'Help', submenu: helpMenuItems },
],
);
......@@ -445,12 +495,55 @@ export function install() {
// Called by reload above
function reset() {
fileMenuItems_custom.length = 0;
editMenuItems_custom.length = 0;
viewMenuItems_custom.length = 0;
fileMenuItemsCustom.length = 0;
editMenuItemsCustom.length = 0;
viewMenuItemsCustom.length = 0;
customMenus.length = 0;
customItems.clear();
install();
}
// --------------------------------------------------------------------------
// --- Popup Menu Management
// --------------------------------------------------------------------------
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;
}
type PopupMenuItem = PopupMenuItemProps | 'separator';
function handlePopupMenu(_: Event, items: PopupMenuItem[]): Promise<number> {
return new Promise((resolve) => {
const menu = new Menu();
let kid = 0;
let selected = (-1);
items.forEach((item, index) => {
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 click = () => { selected = index; };
const type = checked !== undefined ? 'checkbox' : 'normal';
menu.append(new MenuItem({ label, enabled, type, checked, click }));
}
}
});
menu.popup({ callback: () => resolve(selected) });
});
}
ipcMain.handle('dome.popup', handlePopupMenu);
// --------------------------------------------------------------------------
......@@ -36,9 +36,6 @@ import Emitter from 'events';
import Exec from 'child_process';
import fspath from 'path';
import fs from 'fs';
import { app, remote } from 'electron';
declare const __static: string;
// --------------------------------------------------------------------------
// --- Platform Specificities
......@@ -121,32 +118,11 @@ export function doExit() {
let COMMAND_WDIR = '.';
let COMMAND_ARGV: string[] = [];
function SET_COMMAND(argv: string[], wdir: string) {
function setCommandLine(argv: string[], wdir: string) {
COMMAND_ARGV = argv;
COMMAND_WDIR = wdir;
}
// --------------------------------------------------------------------------
// --- User's Directories
// --------------------------------------------------------------------------
const appProxy = app || remote.app;
/** Returns user's home directory. */
export function getHome() { return appProxy.getPath('home'); }
/** Returns user's desktop directory. */
export function getDesktop() { return appProxy.getPath('desktop'); }
/** Returns user's documents directory. */
export function getDocuments() { return appProxy.getPath('documents'); }
/** Returns user's downloads directory. */
export function getDownloads() { return appProxy.getPath('downloads'); }
/** Returns temporary directory. */
export function getTempDir() { return appProxy.getPath('temp'); }
/**
Working directory (Application Window).
......@@ -177,17 +153,6 @@ export function getPID() { return process.pid; }
*/
export function getArguments() { return COMMAND_ARGV; }
/** Returns path of static assets.
Returns the path to the associated `./static/<...path>` of your
application. The `./static/` directory is automatically packed
into your application by Dome thanks to `electron-webpack` default
configuration.
*/
export function getStatic(...path: string[]) {
return fspath.join(__static, ...path);
}
// --------------------------------------------------------------------------
// --- File Join
// --------------------------------------------------------------------------
......@@ -308,14 +273,10 @@ export function exists(path: string) {
/**
Reads a textual file contents.
Promisified
[Node `fs.readFile`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_readfile_path_options_callback)
using `UTF-8` encoding.
Promisified `fs.readFile` using `utf-8` encoding.
*/
export function readFile(path: string): Promise<string> {
return new Promise((result, reject) => {
fs.readFile(path, 'UTF-8', (err, data) => (err ? reject(err) : result(data)));
});
return fs.promises.readFile(path, { encoding: 'utf-8' });
}
// --------------------------------------------------------------------------
......@@ -325,14 +286,10 @@ export function readFile(path: string): Promise<string> {
/**
Writes a textual content in a file.
Promisified
[Node `fs.writeFile`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_writefile_file_data_options_callback)
using `UTF-8` encoding.
Promisified `fs.writeFile` using `utf-8` encoding.
*/
export function writeFile(path: string, content: string): Promise<void> {
return new Promise((result, reject) => {
fs.writeFile(path, content, 'UTF-8', (err) => (err ? reject(err) : result()));
});
export async function writeFile(path: string, content: string): Promise<void> {
return fs.promises.writeFile(path, content, { encoding: 'utf-8' });
}
// --------------------------------------------------------------------------
......@@ -344,14 +301,10 @@ export function writeFile(path: string, content: string): Promise<void> {
@param srcPath - the source file path
@param tgtPath - the target file path
Promisified
[Node `fs.copyFile`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_copyfile_src_dest_flags_callback)
using `UTF-8` encoding.
Promisified `fs.copyFile`.
*/
export function copyFile(srcPath: string, tgtPath: string): Promise<void> {
return new Promise((result, reject) => {
fs.copyFile(srcPath, tgtPath, (err) => (err ? reject(err) : result()));
});
export async function copyFile(srcPath: string, tgtPath: string): Promise<void> {
return fs.promises.copyFile(srcPath, tgtPath);
}
// --------------------------------------------------------------------------
......@@ -362,24 +315,15 @@ export function copyFile(srcPath: string, tgtPath: string): Promise<void> {
Reads a directory.
@returns directory contents (local names)
Promisified
[Node `fs.readdir`](https://nodejs.org/dist/latest-v12.x/docs/api/fs.html#fs_fs_readdir_path_options_callback).
Promisified `fs.readdir`.
Uses `UTF-8` encoding to obtain (relative) file names instead of byte buffers. On MacOS, `.DS_Store` entries
are filtered out.
Uses `utf-8` encoding to obtain (relative) file names instead of byte buffers.
On MacOS, `.DS_Store` entries are filtered out.
*/
export function readDir(path: string): Promise<string[]> {
export async function readDir(path: string): Promise<string[]> {
const filterDir = (f: string) => f !== '.DS_Store';
return new Promise((result, reject) => {
fs.readdir(
path,
{ encoding: 'UTF-8', withFileTypes: true },
(err: NodeJS.ErrnoException | null, files: fs.Dirent[]) => {
if (err) reject(err);
else result(files.map((fn) => fn.name).filter(filterDir));
},
);
});
const entries = await fs.promises.readdir(path, { encoding: 'utf-8', withFileTypes: true });
return entries.map((fn) => fn.name).filter(filterDir);
}
// --------------------------------------------------------------------------
......@@ -441,7 +385,8 @@ async function rmDirRec(path: string): Promise<void> {
try {
const stats = fs.statSync(path);
if (stats.isFile()) {
return remove(path);
await remove(path);
return;
}
if (stats.isDirectory()) {
const rmDirSub = (name: string) => {
......@@ -506,9 +451,9 @@ atExit(() => {
export type StdPipe = { path?: string | undefined; mode?: number; pipe?: boolean };
export type StdOptions = undefined | 'null' | 'ignore' | 'pipe' | StdPipe;
type stdio = { io: number | 'pipe' | 'ignore' | 'ipc'; fd?: number };
type StdIO = { io: number | 'pipe' | 'ignore' | 'ipc'; fd?: number };
function stdSpec(spec: StdOptions, isOutput: boolean): stdio {
function stdSpec(spec: StdOptions, isOutput: boolean): StdIO {
switch (spec) {
case undefined:
return { io: isOutput ? 'pipe' : 'ignore' };
......@@ -533,7 +478,7 @@ interface Readable {
function pipeTee(std: Readable, fd: number) {
if (!fd) return;
const out = fs.createWriteStream('<ignored>', { fd, encoding: 'UTF-8' });
const out = fs.createWriteStream('<ignored>', { fd, encoding: 'utf-8' });
out.on('error', (err) => {
console.warn('[Dome] can not pipe:', err);
std.unpipe(out);
......@@ -627,11 +572,11 @@ export function spawn(
const err = child.stderr;
if (out && stdout.fd) {
out.setEncoding('UTF-8');
out.setEncoding('utf-8');
pipeTee(out, stdout.fd);
}
if (err && stderr.fd) {
err.setEncoding('UTF-8');
err.setEncoding('utf-8');
pipeTee(err, stderr.fd);
}
......@@ -651,7 +596,7 @@ const WINDOW_PREFERENCES_ARGV = '--dome-preferences-window';
// --------------------------------------------------------------------------
export default {
SET_COMMAND,
setCommandLine,
WINDOW_APPLICATION_ARGV,
WINDOW_PREFERENCES_ARGV,
};
......
......@@ -31,9 +31,9 @@
import type { CSSProperties } from 'react';
type falsy = undefined | boolean | null | '';
type Falsy = undefined | boolean | null | '';
export type ClassSpec = string | falsy | { [cname: string]: true | falsy };
export type ClassSpec = string | Falsy | { [cname: string]: true | Falsy };
/**
Utility function to merge various HTML class properties
......@@ -75,7 +75,7 @@ export function classes(
return buffer.join(' ');
}
export type StyleSpec = falsy | CSSProperties;
export type StyleSpec = Falsy | CSSProperties;
/**
Utility function to merge various CSS style properties
......
......@@ -32,7 +32,6 @@
import React from 'react';
import { classes } from 'dome/misc/utils';
import { Icon } from './icons';
import { LabelProps } from './labels';
import './style.css';
interface EVENT {
......@@ -48,68 +47,6 @@ const TRIGGER = (onClick?: () => void) => (evt?: EVENT) => {
if (onClick) onClick();
};
// --------------------------------------------------------------------------
// --- LCD
// --------------------------------------------------------------------------
/** Button-like label. */
export function LCD(props: LabelProps) {
const className = classes(
'dome-xButton dome-xBoxButton dome-text-code dome-xButton-lcd ',
props.className,
);
return (
<label
className={className}
title={props.title}
style={props.style}
>
{props.icon && <Icon id={props.icon} />}
{props.label}
{props.children}
</label>
);
}
// --------------------------------------------------------------------------
// --- Led
// --------------------------------------------------------------------------
export type LEDstatus =
undefined | 'inactive' | 'active' | 'positive' | 'negative' | 'warning';
export interface LEDprops {
/**
Led status:
- `'inactive'`: off (default)
- `'active'`: blue color
- `'positive'`: green color
- `'negative'`: red color
- `'warning'`: orange color
*/
status?: LEDstatus;
/** Blinking Led (default: `false`). */
blink?: boolean;
/** Tooltip text. */
title?: string;
/** Additional CSS class. */
className?: string;
/** Additional CSS style. */
style?: React.CSSProperties;
}
export const LED = (props: LEDprops) => {
const className = classes(
'dome-xButton-led',
`dome-xButton-led-${props.status || 'inactive'}`,
props.blink && 'dome-xButton-blink',
props.className,
);
return (
<div className={className} title={props.title} style={props.style} />
);
};
// --------------------------------------------------------------------------
// --- Button
// --------------------------------------------------------------------------
......@@ -138,7 +75,7 @@ const LABEL = ({ disabled, label }: LABELprops) => (
);
export type ButtonKind =
'default' | 'active' | 'primary' | 'warning' | 'positive' | 'negative';
'default' | 'primary' | 'warning' | 'positive' | 'negative';
export interface ButtonProps {
/** Text of the label. Prepend to other children elements. */
......@@ -169,7 +106,6 @@ export interface ButtonProps {
focusable?: boolean;
/** Styled bytton:
- `'default'`: normal button;
- `'active'`: active normal button;
- `'primary'`: primary button, in blue;
- `'warning'`: warning button, in orange;
- `'positive'`: positive button, in green;
......@@ -558,15 +494,13 @@ export interface SelectProps {
* <optgroup label='…'>…</optgroup>
* <option value='…' disabled=… >…</option>
**Warning:** most non-positionning CSS properties might not
work on the`<select>` element due to the native rendering used
by Chrome.
You might use `-webkit-appearance: none` to cancel this behavior,
you will have to restyle the
component entirely, which is quite ugly by default.
*/
export function Select(props: SelectProps) {
const { onChange, className = '', placeholder } = props;
const { onChange, placeholder } = props;
const className = classes(
'dome-xSelect dome-xBoxButton dome-xButton-default dome-xButton-label',
props.className,
);
const disabled = onChange ? DISABLED(props) : true;
const callback = (evt: React.ChangeEvent<HTMLSelectElement>) => {
if (onChange) onChange(evt.target.value);
......@@ -575,7 +509,7 @@ export function Select(props: SelectProps) {
<select
id={props.id}
disabled={disabled}
className={`dome - xSelect ${className} `}
className={className}
style={props.style}
title={props.title}
value={props.value}
......@@ -583,7 +517,7 @@ export function Select(props: SelectProps) {
>
{placeholder && <option value="">{placeholder}</option>}
{props.children}
</select>
</select >
);
}
......
/* ************************************************************************ */
/* */
/* This file is part of Frama-C. */
/* */
/* Copyright (C) 2007-2021 */
/* CEA (Commissariat à l'énergie atomique et aux énergies */
/* alternatives) */
/* */
/* you can redistribute it and/or modify it under the terms of the GNU */
/* Lesser General Public License as published by the Free Software */
/* Foundation, version 2.1. */
/* */
/* It is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU Lesser General Public License for more details. */
/* */
/* See the GNU Lesser General Public License version 2.1 */
/* for more details (enclosed in the file licenses/LGPLv2.1). */
/* */
/* ************************************************************************ */
// --------------------------------------------------------------------------
// --- LEDs, LCD, meters, etc.
// --------------------------------------------------------------------------
/**
@packageDocumentation
@module dome/controls/displays
*/
import React from 'react';
import { classes } from 'dome/misc/utils';
import { Icon } from './icons';
import { LabelProps } from './labels';
import './style.css';
// --------------------------------------------------------------------------
// --- LCD
// --------------------------------------------------------------------------
/** Button-like label. */
export function LCD(props: LabelProps) {
const className = classes(
'dome-xButton dome-xBoxButton dome-text-code dome-xButton-lcd ',
props.className,
);
return (
<label
className={className}
title={props.title}
style={props.style}
>
{props.icon && <Icon id={props.icon} />}
{props.label}
{props.children}
</label>
);
}
// --------------------------------------------------------------------------
// --- Led
// --------------------------------------------------------------------------
export type LEDstatus =
undefined | 'inactive' | 'active' | 'positive' | 'negative' | 'warning';
export interface LEDprops {
/**
Led status:
- `'inactive'`: off (default)
- `'active'`: blue color
- `'positive'`: green color
- `'negative'`: red color
- `'warning'`: orange color
*/
status?: LEDstatus;
/** Blinking Led (default: `false`). */
blink?: boolean;
/** Tooltip text. */
title?: string;
/** Additional CSS class. */
className?: string;
/** Additional CSS style. */
style?: React.CSSProperties;
}
export const LED = (props: LEDprops) => {
const className = classes(
'dome-xButton-led',
`dome-xButton-led-${props.status || 'inactive'}`,
props.blink && 'dome-xButton-blink',
props.className,
);
return (
<div className={className} title={props.title} style={props.style} />
);
};
// --------------------------------------------------------------------------
// --- Metter
// --------------------------------------------------------------------------
export interface MeterProps {
/** Additional CSS class. */
className?: string;
/** Additional CSS style. */
style?: React.CSSProperties;
/** Disabled control. */
/** Meter value. Undefined means disabled. */
value: number; /** default is undefined */
min?: number; /** default is 0.0 */
low?: number; /** default is 0.0 */
high?: number; /** default is 1.0 */
max?: number; /** default is 1.0 */
optimum?: number | 'LOW' | 'MEDIUM' | 'HIGH'; /** default is undefined */
}
export const Meter = (props: MeterProps) => {
const { className, style, value, optimum, ...ms } = props;
const min = props.min ?? 0.0;
const max = props.max ?? 1.0;
const low = props.low ?? min;
const hight = props.high ?? max;
const theClass = classes('dome-xMeter', className);
let opt: number | undefined;
if (value !== undefined)
switch (optimum) {
case 'LOW': opt = (min + low) / 2; break;
case 'MEDIUM': opt = (low + hight) / 2; break;
case 'HIGH': opt = (hight + max) / 2; break;
default: opt = optimum;
}
const mv = value === undefined ? min : Math.min(max, Math.max(min, value));
return (
<meter
className={theClass}
style={style}
value={mv}
optimum={opt}
{...ms} />
);
};
// --------------------------------------------------------------------------
......@@ -25,9 +25,11 @@
// --------------------------------------------------------------------------
/**
Consult the [Icon Gallery](../guides/icons.md.html) for default icons.
You can [register](#.register) new icons or override existing ones
and [iterate](#.forEach) over the icon base.
[[include:icons.md]]
@packageDocumentation
@module dome/controls/icons
*/
......
......@@ -248,6 +248,38 @@
background: radial-gradient( circle at center , #ffc749 , #ecd44f );
}
/* -------------------------------------------------------------------------- */
/* --- Styling Meters --- */
/* -------------------------------------------------------------------------- */
meter.dome-xMeter {
background: white;
border-radius: 5px;
box-shadow:
0 0 4px 4px rgba(0,0,0,0.15) inset;
height: 16px;
width: 70px;
}
meter.dome-xMeter::-webkit-meter-bar {
background: transparent;
border-radius: 5px;
height: 14px;
}
meter.dome-xMeter::-webkit-meter-optimum-value {
background: linear-gradient(to bottom, #4a0 0%, #8f0 20%, #4a0 100%);
}
meter.dome-xMeter::-webkit-meter-suboptimum-value {
background: linear-gradient(to bottom, #aa0 0%, #ff0 20%, #aa0 100%);
}
meter.dome-xMeter::-webkit-meter-even-less-good-value {
background: linear-gradient(to bottom, #a40 0%, #f80 20%, #a40 100%);
}
/* -------------------------------------------------------------------------- */
/* --- Styling Checkbox --- */
/* -------------------------------------------------------------------------- */
......
......@@ -56,10 +56,10 @@ export const isEqual = FastCompare;
export function equal(_x: any, _y: any): 0 { return 0; }
/** Primitive comparison works on this type. */
export type bignum = bigint | number;
export type BigNum = bigint | number;
/** Detect Non-NaN numbers and big-ints. */
export function isBigNum(x: any): x is bignum {
export function isBigNum(x: any): x is BigNum {
return (
(typeof (x) === 'bigint') ||
(typeof (x) === 'number' && !Number.isNaN(x))
......@@ -91,7 +91,7 @@ export const string: Order<string> = primitive;
/**
Primitive comparison for (big) integers (non NaN numbers included).
*/
export const bignum: Order<bignum> = primitive;
export const bignum: Order<BigNum> = primitive;
/**
Primitive comparison for number (NaN included).
......@@ -291,13 +291,13 @@ export function byAllFields<A>(order: ByAllFields<A>): Order<A> {
};
}
export type dict<A> = undefined | null | { [key: string]: A };
export type Dict<A> = undefined | null | { [key: string]: A };
/**
Compare dictionaries _wrt_ lexicographic order of entries.
*/
export function dictionary<A>(order: Order<A>): Order<dict<A>> {
return (x: dict<A>, y: dict<A>) => {
export function dictionary<A>(order: Order<A>): Order<Dict<A>> {
return (x: Dict<A>, y: Dict<A>) => {
if (x === y) return 0;
const dx = x ?? {};
const dy = y ?? {};
......@@ -398,29 +398,31 @@ export function tuple5<A, B, C, D, E>(
// --- Structural Comparison
// --------------------------------------------------------------------------
/* eslint-disable no-shadow */
/** @internal */
enum RANK {
enum IRANK {
UNDEFINED,
BOOLEAN, SYMBOL, NAN, BIGNUM,
STRING,
ARRAY, OBJECT, FUNCTION
ARRAY, OBJECT, FUNCTION,
}
/** @internal */
function rank(x: any): RANK {
function rank(x: any): IRANK {
const t = typeof x;
switch (t) {
case 'undefined': return RANK.UNDEFINED;
case 'boolean': return RANK.BOOLEAN;
case 'symbol': return RANK.SYMBOL;
case 'undefined': return IRANK.UNDEFINED;
case 'boolean': return IRANK.BOOLEAN;
case 'symbol': return IRANK.SYMBOL;
case 'number':
return Number.isNaN(x) ? RANK.NAN : RANK.BIGNUM;
return Number.isNaN(x) ? IRANK.NAN : IRANK.BIGNUM;
case 'bigint':
return RANK.BIGNUM;
case 'string': return RANK.STRING;
case 'function': return RANK.FUNCTION;
return IRANK.BIGNUM;
case 'string': return IRANK.STRING;
case 'function': return IRANK.FUNCTION;
case 'object':
return Array.isArray(x) ? RANK.ARRAY : RANK.OBJECT;
return Array.isArray(x) ? IRANK.ARRAY : IRANK.OBJECT;
}
}
......
......@@ -32,6 +32,8 @@
import { DEVEL } from 'dome/system';
/* eslint-disable @typescript-eslint/naming-convention, no-shadow */
export type json =
undefined | null | boolean | number | string |
json[] | { [key: string]: json };
......
......@@ -27,7 +27,7 @@
*/
import filepath from 'path';
import { remote } from 'electron';
import { ipcRenderer } from 'electron';
import * as System from 'dome/system';
// --------------------------------------------------------------------------
......@@ -122,10 +122,9 @@ export async function showMessageBox<A>(
if (cancelId === defaultId) cancelId = -1;
return remote.dialog.showMessageBox(
remote.getCurrentWindow(),
return ipcRenderer.invoke('dome.dialog.showMessageBox',
{
type: kind,
'type': kind,
message,
detail: details,
defaultId,
......@@ -197,8 +196,7 @@ export async function showOpenFile(
props: OpenFileProps,
): Promise<string | undefined> {
const { title, label, path, hidden = false, filters } = props;
return remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
return ipcRenderer.invoke('dome.dialog.showOpenDialog',
{
title,
buttonLabel: label,
......@@ -221,8 +219,7 @@ export async function showOpenFiles(
): Promise<string[] | undefined> {
const { title, label, path, hidden, filters } = props;
return remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
return ipcRenderer.invoke('dome.dialog.showOpenDialog',
{
title,
buttonLabel: label,
......@@ -258,8 +255,7 @@ export async function showSaveFile(
props: SaveFileProps,
): Promise<string | undefined> {
const { title, label, path, filters } = props;
return remote.dialog.showSaveDialog(
remote.getCurrentWindow(),
return ipcRenderer.invoke('dome.dialog.showSaveDialog',
{
title,
buttonLabel: label,
......@@ -292,8 +288,7 @@ export async function showOpenDir(
default: break;
}
return remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
return ipcRenderer.invoke('dome.dialog.showOpenDialog',
{
title,
buttonLabel: label,
......
......@@ -42,7 +42,7 @@ import _ from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { remote, ipcRenderer } from 'electron';
import { ipcRenderer } from 'electron';
import SYS, * as System from 'dome/system';
import * as Json from 'dome/data/json';
import * as Settings from 'dome/data/settings';
......@@ -77,14 +77,50 @@ export type PlatformKind = 'linux' | 'macos' | 'windows';
/** System platform. */
export const platform: PlatformKind = (System.platform as PlatformKind);
// --------------------------------------------------------------------------
// --- User's Directories
// --------------------------------------------------------------------------
let loadedPaths = false;
const remoteAppPaths: { [index: string]: string } = {};
function getPath(k: string): string {
if (!loadedPaths) {
loadedPaths = true;
Object.assign(remoteAppPaths, ipcRenderer.sendSync('dome.app.paths'));
}
return remoteAppPaths[k];
}
/** Returns user's home directory. */
export function getHome() { return getPath('home'); }
/** Returns user's desktop directory. */
export function getDesktop() { return getPath('desktop'); }
/** Returns user's documents directory. */
export function getDocuments() { return getPath('documents'); }
/** Returns user's downloads directory. */
export function getDownloads() { return getPath('downloads'); }
/** Returns temporary directory. */
export function getTempDir() { return getPath('temp'); }
/** Working directory (Application Window). */
export function getWorkingDir() { return System.getWorkingDir(); }
/** Current process ID.. */
export function getPID() { return System.getPID(); }
// --------------------------------------------------------------------------
// --- Application Emitter
// --------------------------------------------------------------------------
/** Typed Dome Event.
To register an event with no argument, simply use `new Event('myEvent')`.
*/
To register an event with no argument, simply use `new Event('myEvent')`.
*/
export class Event<A = void> {
private name: string;
......@@ -168,7 +204,7 @@ export function onCommand(
}
ipcRenderer.on('dome.ipc.command', (_event, argv, wdir) => {
SYS.SET_COMMAND(argv, wdir);
SYS.setCommandLine(argv, wdir);
System.emitter.emit('dome.command', argv, wdir);
});
......@@ -467,29 +503,27 @@ export function popupMenu(
items: PopupMenuItem[],
callback?: (item: string | undefined) => void,
) {
const { Menu, MenuItem } = remote;
const menu = new Menu();
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 id = item.id || label;
const click = () => {
selected = id;
if (item.onClick) item.onClick();
};
const type = checked !== undefined ? 'checkbox' : 'normal';
menu.append(new MenuItem({ label, enabled, type, checked, click }));
}
const ipcItems = items.map((item) => {
if (!item) return undefined;
if (item === 'separator') return item;
return {
label: item.label,
id: item.id,
display: !!(item.display ?? true),
enabled: !!(item.enabled ?? true),
checked: !!(item.checked ?? false),
};
});
ipcRenderer.invoke('dome.popup', ipcItems).then((index: number) => {
const item = items[index];
if (item && item !== 'separator') {
const { id, label, onClick } = item;
if (onClick) onClick();
if (callback) callback(id || label);
} else {
if (callback) callback(undefined);
}
});
const job = callback ? () => callback(selected) : undefined;
menu.popup({ window: remote.getCurrentWindow(), callback: job });
}
// --------------------------------------------------------------------------
......
......@@ -60,7 +60,7 @@ interface CatchState {
/**
React Error Boundaries.
*/
export class Catch extends React.Component<CatchProps, CatchState, {}> {
export class Catch extends React.Component<CatchProps, CatchState, unknown> {
constructor(props: CatchProps) {
super(props);
......
......@@ -68,7 +68,7 @@ export function SideBar(props: SideBarProps) {
// --------------------------------------------------------------------------
export type BadgeElt = undefined | null | string | number | React.ReactNode;
export type Badge = BadgeElt | BadgeElt[];
export type Badges = BadgeElt | BadgeElt[];
const makeBadgeElt = (elt: BadgeElt): React.ReactNode => {
if (elt === undefined || elt === null) return null;
......@@ -81,7 +81,7 @@ const makeBadgeElt = (elt: BadgeElt): React.ReactNode => {
}
};
const makeBadge = (elt: Badge): React.ReactNode => {
const makeBadge = (elt: Badges): React.ReactNode => {
if (Array.isArray(elt))
return elt.map(makeBadgeElt);
return makeBadgeElt(elt);
......@@ -120,7 +120,7 @@ export interface SectionProps {
/** Disabled sections are made unvisible. */
disabled?: boolean;
/** Badge summary (only visible when folded). */
summary?: Badge;
summary?: Badges;
/** Right-click callback. */
onContextMenu?: () => void;
/** Section contents. */
......@@ -182,7 +182,7 @@ export interface ItemProps {
/** Item tooltip text. */
title?: string;
/** Badge. */
badge?: Badge;
badge?: Badges;
/** Enabled item. */
enabled?: boolean;
/** Disabled item (dimmed). */
......
......@@ -252,7 +252,7 @@
}
.dome-xToolBar-vrule {
width: 2px ;
width: 1px ;
position: relative ;
left: 4px ;
height: 18px ;
......
......@@ -31,7 +31,6 @@
import React from 'react';
import { Event, useEvent, find } from 'dome';
import { debounce } from 'lodash';
import { SVG } from 'dome/controls/icons';
import { Label } from 'dome/controls/labels';
import { classes } from 'dome/misc/utils';
......@@ -95,12 +94,6 @@ const KIND = (kind: undefined | string) => (
kind ? ` dome-xToolBar-${kind}` : ''
);
interface SELECT<A> {
selected?: boolean;
selection?: A;
value?: A;
}
export type ButtonKind =
| 'default' | 'cancel' | 'warning' | 'positive' | 'negative';
......@@ -268,6 +261,12 @@ export interface SearchFieldProps<A> {
event?: null | Event<void>;
}
interface Searching {
pattern?: string;
timer?: NodeJS.Timeout | undefined;
onSearch?: ((p: string) => void);
}
/**
Search Bar.
*/
......@@ -277,18 +276,24 @@ export function SearchField<A = undefined>(props: SearchFieldProps<A>) {
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(
debounce((pattern: string) => {
if (onSearch) onSearch(pattern);
}, DEBOUNCED_SEARCH),
[onSearch],
);
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]);
// Blur Event
const onBlur = () => {
......
......@@ -45,12 +45,12 @@ import * as Utils from 'dome/misc/utils';
import { SVG } from 'dome/controls/icons';
import { Checkbox, Radio, Select as SelectMenu } from 'dome/controls/buttons';
export type Error =
export type FieldError =
| undefined | boolean | string
| { [key: string]: Error } | Error[];
export type Checker<A> = (value: A) => boolean | Error;
export type Callback<A> = (value: A, error: Error) => void;
export type FieldState<A> = [A, Error, Callback<A>];
| { [key: string]: FieldError } | FieldError[];
export type Checker<A> = (value: A) => boolean | FieldError;
export type Callback<A> = (value: A, error: FieldError) => void;
export type FieldState<A> = [A, FieldError, Callback<A>];
/* --------------------------------------------------------------------------*/
/* --- State Errors Utilities ---*/
......@@ -66,28 +66,28 @@ export function inRange(
export function validate<A>(
value: A,
checker: undefined | Checker<A>,
): Error {
): FieldError {
if (checker) {
try {
const r = checker(value);
if (r === undefined || r === true) return undefined;
return r;
} catch (err) {
return err.toString() || false;
return `${err}`;
}
}
return undefined;
}
export function isValid(err: Error): boolean { return !err; }
export function isValid(err: FieldError): boolean { return !err; }
type ObjectError = { [key: string]: Error };
type ObjectError = { [key: string]: FieldError };
function isObjectError(err: Error): err is ObjectError {
function isObjectError(err: FieldError): err is ObjectError {
return typeof err === 'object' && !Array.isArray(err);
}
function isArrayError(err: Error): err is Error[] {
function isArrayError(err: FieldError): err is FieldError[] {
return Array.isArray(err);
}
......@@ -99,7 +99,7 @@ function isValidObject(err: ObjectError): boolean {
return true;
}
function isValidArray(err: Error[]): boolean {
function isValidArray(err: FieldError[]): boolean {
for (let k = 0; k < err.length; k++) {
if (!isValid(err[k])) return false;
}
......@@ -117,8 +117,8 @@ export function useState<A>(
onChange?: Callback<A>,
): FieldState<A> {
const [value, setValue] = React.useState<A>(defaultValue);
const [error, setError] = React.useState<Error>(undefined);
const setState = React.useCallback((newValue: A, newError: Error) => {
const [error, setError] = React.useState<FieldError>(undefined);
const setState = React.useCallback((newValue: A, newError: FieldError) => {
const localError = validate(newValue, checker) || newError;
setValue(newValue);
setError(localError);
......@@ -133,9 +133,9 @@ export function useValid<A>(
): FieldState<A> {
const [value, setValue] = state;
const [local, setLocal] = React.useState(value);
const [error, setError] = React.useState<Error>(undefined);
const [error, setError] = React.useState<FieldError>(undefined);
const update = React.useCallback(
(newValue: A, newError: Error) => {
(newValue: A, newError: FieldError) => {
setLocal(newValue);
setError(newError);
if (!newError) setValue(newValue);
......@@ -162,7 +162,7 @@ export function useDefined<A>(
): FieldState<A | undefined> {
const [value, error, setState] = state;
const update = React.useCallback(
(newValue: A | undefined, newError: Error) => {
(newValue: A | undefined, newError: FieldError) => {
if (newValue !== undefined) {
setState(newValue, newError);
}
......@@ -182,7 +182,7 @@ export function useRequired<A>(
const [value, error, setState] = state;
const cache = React.useRef(value);
const update = React.useCallback(
(newValue: A | undefined, newError: Error) => {
(newValue: A | undefined, newError: FieldError) => {
if (newValue === undefined) {
setState(cache.current, onError || 'Required field');
} else {
......@@ -202,7 +202,7 @@ export function useChecker<A>(
checker?: Checker<A>,
): FieldState<A> {
const [value, error, setState] = state;
const update = React.useCallback((newValue: A, newError: Error) => {
const update = React.useCallback((newValue: A, newError: FieldError) => {
const localError = validate(newValue, checker) || newError;
setState(newValue, localError);
}, [checker, setState]);
......@@ -235,11 +235,11 @@ export function useFilter<A, B>(
const [value, error, setState] = state;
const [localValue, setLocalValue] = React.useState(defaultValue);
const [localError, setLocalError] = React.useState<Error>(undefined);
const [localError, setLocalError] = React.useState<FieldError>(undefined);
const [dangling, setDangling] = React.useState(false);
const update = React.useCallback(
(newValue: B, newError: Error) => {
(newValue: B, newError: FieldError) => {
try {
const outValue = output(newValue);
setLocalValue(newValue);
......@@ -250,7 +250,7 @@ export function useFilter<A, B>(
}
} catch (err) {
setLocalValue(newValue);
setLocalError(newError || err.toString() || 'Invalid value');
setLocalError(newError || err ? `${err}` : 'Invalid value');
setDangling(true);
}
}, [output, setState, setLocalValue, setLocalError],
......@@ -262,7 +262,7 @@ export function useFilter<A, B>(
try {
return [input(value), error, update];
} catch (err) {
return [localValue, err.toString() || 'Invalid input', update];
return [localValue, err ? `${err}` : 'Invalid input', update];
}
}
......@@ -284,12 +284,12 @@ export function useLatency<A>(
const update = React.useMemo(() => {
if (period > 0) {
const propagate = debounce(
(lateValue: A, lateError: Error) => {
(lateValue: A, lateError: FieldError) => {
setState(lateValue, lateError);
setDangling(false);
}, period,
);
return (newValue: A, newError: Error) => {
return (newValue: A, newError: FieldError) => {
setLocalValue(newValue);
setLocalError(newError);
setDangling(true);
......@@ -315,7 +315,7 @@ export function useProperty<A, K extends keyof A>(
checker?: Checker<A[K]>,
): FieldState<A[K]> {
const [value, error, setState] = state;
const update = React.useCallback((newProp: A[K], newError: Error) => {
const update = React.useCallback((newProp: A[K], newError: FieldError) => {
const newValue = { ...value, [property]: newProp };
const objError = isObjectError(error) ? error : {};
const propError = validate(newProp, checker) || newError;
......@@ -334,7 +334,7 @@ export function useIndex<A>(
checker?: Checker<A>,
): FieldState<A> {
const [array, error, setState] = state;
const update = React.useCallback((newValue: A, newError: Error) => {
const update = React.useCallback((newValue: A, newError: FieldError) => {
const newArray = array.slice();
newArray[index] = newValue;
const localError = isArrayError(error) ? error.slice() : [];
......@@ -436,7 +436,7 @@ export interface WarningProps {
/** Short warning message (displayed on hover). */
warning?: string;
/** Error details (if a string is provided, in tooltip). */
error?: Error;
error?: FieldError;
/** Label offset. */
offset?: number;
}
......@@ -494,7 +494,7 @@ export interface SectionProps extends FilterProps, Children {
/** Warning Error (when unfolded). */
warning?: string;
/** Associated Error. */
error?: Error;
error?: FieldError;
/** Fold/Unfold settings. */
settings?: string;
/** Fold/Unfold state (defaults to false). */
......@@ -550,7 +550,7 @@ export interface GenericFieldProps extends FilterProps, Children {
/** Warning message (in case of error). */
onError?: string;
/** Error (if any). */
error?: Error;
error?: FieldError;
}
let FIELDID = 0;
......@@ -626,7 +626,7 @@ export interface FieldProps<A> extends FilterProps {
}
type InputEvent = { target: { value: string } };
type InputState = [string, Error, (evt: InputEvent) => void];
type InputState = [string, FieldError, (evt: InputEvent) => void];
function useChangeEvent(setState: Callback<string>) {
return React.useCallback(
......@@ -635,17 +635,6 @@ function useChangeEvent(setState: Callback<string>) {
);
}
function useTextInputField(
props: TextFieldProps,
defaultLatency: number,
): InputState {
const checked = useChecker(props.state, props.checker);
const period = props.latency ?? defaultLatency;
const [value, error, setState] = useLatency(checked, period);
const onChange = useChangeEvent(setState);
return [value || '', error, onChange];
}
/* --------------------------------------------------------------------------*/
/* --- Text Fields ---*/
/* --------------------------------------------------------------------------*/
......@@ -658,6 +647,17 @@ export interface TextFieldProps extends FieldProps<string | undefined> {
latency?: number;
}
function useTextInputField(
props: TextFieldProps,
defaultLatency: number,
): InputState {
const checked = useChecker(props.state, props.checker);
const period = props.latency ?? defaultLatency;
const [value, error, setState] = useLatency(checked, period);
const onChange = useChangeEvent(setState);
return [value || '', error, onChange];
}
/**
Text Field.
@category Text Fields
......
......@@ -43,13 +43,13 @@ import {
export type ByColumns<Row> = { [dataKey: string]: Compare.Order<Row> };
interface PACK<Key, Row> {
export interface PACK<Key, Row> {
index: number | undefined;
key: Key;
row: Row;
}
type SORT<K, R> = Order<PACK<K, R>>;
export type SORT<K, R> = Order<PACK<K, R>>;
function orderBy<K, R>(
columns: ByColumns<R>,
......
......@@ -206,8 +206,8 @@ interface PopupItem {
type PopupMenu = ('separator' | PopupItem)[];
type Cmap<A> = Map<string, A>;
type Cprops = ColProps<any>;
type ColProps<R> = ColumnProps<R, any>;
type Cprops = ColProps<any>;
// --------------------------------------------------------------------------
// --- Column Utilities
......@@ -239,8 +239,8 @@ function makeRowGetter<Key, Row>(model?: Model<Key, Row>) {
}
function makeDataGetter(
getter: ((row: any, dataKey: string) => any) = defaultGetter,
dataKey: string,
getter: ((row: any, dataKey: string) => any) = defaultGetter,
): TableCellDataGetter {
return (({ rowData }) => {
try {
......@@ -644,7 +644,7 @@ class TableState<Key, Row> {
computeGetter(id: string, dataKey: string, props: Cprops) {
const current = this.getter.get(id);
if (current) return current;
const dataGetter = makeDataGetter(props.getter, dataKey);
const dataGetter = makeDataGetter(dataKey, props.getter);
this.getter.set(id, dataGetter);
return dataGetter;
}
......
......@@ -113,6 +113,8 @@ function byVisibleTag(lmin: number, lmax: number) {
// --- Buffer
// --------------------------------------------------------------------------
export type TextMarker = CodeMirror.TextMarker<CodeMirror.MarkerRange>;
export interface RichTextBufferProps {
/**
......@@ -184,7 +186,7 @@ export class RichTextBuffer extends Emitter {
private cssmarkers = new Map<string, CSSMarker>();
// Indexed by marker user identifier
private textmarkers = new Map<string, CodeMirror.TextMarker[]>();
private textmarkers = new Map<string, TextMarker[]>();
private decorator?: Decorator;
private edited = false;
......@@ -327,7 +329,7 @@ export class RichTextBuffer extends Emitter {
/** Lookup for the text markers associated with a marker identifier.
Remove the marked tags from the buffered tag array. */
findTextMarker(id: string): CodeMirror.TextMarker[] {
findTextMarker(id: string): TextMarker[] {
this.doFlushText();
this.bufferedTags.forEach((tg, idx) => {
if (tg?.id === id) {
......@@ -470,8 +472,10 @@ export class RichTextBuffer extends Emitter {
let line = Infinity;
this.findTextMarker(position).forEach((tm) => {
const rg = tm.find();
const ln = rg.from.line;
if (ln < line) line = ln;
if (rg && rg.from) {
const ln = rg.from.line;
if (ln < line) line = ln;
}
});
if (line !== Infinity)
this.emit('scroll', line);
......@@ -497,7 +501,7 @@ export class RichTextBuffer extends Emitter {
private onChange(
_editor: CodeMirror.Editor,
change: CodeMirror.EditorChangeLinkedList,
change: CodeMirror.EditorChange,
) {
if (change.origin !== 'buffer') {
this.setEdited(true);
......