diff --git a/src/renderer/app/settings.ts b/src/renderer/app/settings.ts index fc5f308..ee3b2cc 100644 --- a/src/renderer/app/settings.ts +++ b/src/renderer/app/settings.ts @@ -12,7 +12,7 @@ import { } from "persistence/ConfigDTO"; import { AUTOSAVE_DIR } from "Shared/const"; import { DSUtils } from "util/DSUtils"; -import ColorPaletteEditor, { ColorPicker } from "./color-palette-editor"; +import { ColorPicker } from "./color-palette-editor"; import { GlobalCommandsCtx } from "./global-commands"; import { ToastCtx } from "./toast-context"; @@ -107,11 +107,6 @@ export class Settings extends Component { middleClick: rx.bind(conf.partial('binds', 'middleClick')), }}), ]), - ui5.panel({ fields: { headerText: 'Color Palette', collapsed: true }}, [ - ColorPaletteEditor.t({ - props: { palette: rx.bind(conf.partial('colorPalette')) } - }), - ]), ui5.panel({ fields: { headerText: 'PDF Annotation', collapsed: true }}, [ h.div(ui5.checkbox({ fields: { checked: rx.bind(conf.partial('autoOpenWojWithSameNameAsPDF')), diff --git a/src/renderer/app/toolbars.ts b/src/renderer/app/toolbars.ts index 8c5e92c..eb4124c 100644 --- a/src/renderer/app/toolbars.ts +++ b/src/renderer/app/toolbars.ts @@ -27,6 +27,7 @@ import RecentFiles from "persistence/recent-files"; import imgAutorenew from 'res/icon/material/autorenew.svg'; import imgDefaultPen from 'res/icon/custom/default-pen.svg'; import { ApiClient } from "electron-api-client"; +import ColorPopover, { ColorDef } from "common/color-popover"; @Component.register export default class Toolbars extends Component { @@ -88,6 +89,23 @@ export default class Toolbars extends Component { }); const shapePopoverRef = this.ref(); + const colorPopoverRef = this.ref(); + async function pickColor(showAt: HTMLElement): Promise<'cancel' | ColorDef> { + return new Promise(resolve => { + const pop = colorPopoverRef.current; + function onPicked(e: CustomEvent) { ret(e.detail) }; + function onCancel() { ret('cancel') }; + function ret(val: 'cancel' | ColorDef) { + resolve(val); + pop.removeEventListener('color-selected', onPicked); + pop.removeEventListener('after-close', onCancel); + } + pop.showAt(showAt); + pop.addEventListener('color-selected', onPicked); + pop.addEventListener('after-close', onCancel); + }); + } + return [ h.div({ fields: { className: 'topbar' } }, [ // menu @@ -720,7 +738,22 @@ export default class Toolbars extends Component { }), ToolbarSeperator.t(), - h.fragment(configCtx, config => config.colorPalette.map(col => + ToolbarButton.t({ + fields: { id: 'btn-color-1' }, + props: { img: `color:#000000`, alt: 'alt', current: false }, + events: { + click: async _ => { + if (colorPopoverRef.current.open) { + colorPopoverRef.current.close(); + return; + } + const resp = await pickColor(await this.query('#btn-color-1')); + if (resp !== 'cancel') api.setColorByHex(resp.color); + } + } + }), + + h.fragment(configCtx, config => config.colorPaletteWrite.map(col => ToolbarButton.t({ props: { img: `color:${col.color}`, alt: col.name, @@ -733,6 +766,15 @@ export default class Toolbars extends Component { ]), + ColorPopover.t({ + ref: colorPopoverRef, + style: { marginTop: '.3em' }, + props: { + palette: configCtx.partial('colorPaletteWrite'), + currentColor: strokeColor, + } + }), + ui5.popover({ ref: shapePopoverRef, id: 'popover-shape', diff --git a/src/renderer/common/color-popover.ts b/src/renderer/common/color-popover.ts new file mode 100755 index 0000000..341a2f3 --- /dev/null +++ b/src/renderer/common/color-popover.ts @@ -0,0 +1,245 @@ +import { Component, style, rx, h, TemplateElementChild } from '@mvuijs/core'; +import * as ui5 from '@mvuijs/ui5'; +import { theme } from 'global-styles'; +import { BasicDialogManagerContext, DialogButtons } from './dialog-manager'; + +export type ColorDef = { name: string, color: string }; +export type ColorPalette = ColorDef[]; + +export type PopoverPlacementType = 'Left' | 'Right' | 'Top' | 'Bottom'; + +const colorHtmlId = (color: string) => `color-${color.slice(1)}`; + +@Component.register +export default class ColorPopover extends Component<{ + events: { + 'color-selected': CustomEvent, + 'after-close': CustomEvent, + }, +}> { + props = { + placementType: rx.prop({ defaultValue: 'Bottom' }), + palette: rx.prop(), + currentColor: rx.prop(), + } + + showAt(el: HTMLElement) { this.popoverRef.current.showAt(el); } + get open() { return this.popoverRef.current.open } + close() { return this.popoverRef.current.close() } + + private popoverRef = this.ref(); + private editing = new rx.State(false); + + render() { + const { palette, placementType, currentColor } = this.props; + const dlg = this.getContext(BasicDialogManagerContext); + + const openColorPickerDialog = async ( + color: ColorDef, showDeleteBtn: boolean, + ): Promise<'delete' | 'cancel' | ColorDef> => { + const colorPickerColor = new rx.State({...color}); + return new Promise(resolve => { + dlg.openDialog(close => { + const dlgButtons: DialogButtons = [ + { + name: 'Cancel', + action: () => { resolve('cancel'); close(); }, + }, + { + name: 'Ok', + design: 'Emphasized', + action: () => { + resolve({ ...colorPickerColor.value }); + close(); + }, + }, + ]; + if (showDeleteBtn) dlgButtons.unshift({ + name: 'Delete Color', + design: 'Negative', + action: () => { resolve('delete'); close(); }, + icon: 'delete', + }); + return { + heading: '', + buttons: dlgButtons, + onCloseNoBtn: () => resolve('cancel'), + content: [ + h.section( + { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + } + }, + [ + ui5.input({ + fields: { + value: rx.bind(colorPickerColor.partial('name')), + placeholder: 'Name Color', + } + }), + ui5.colorPicker({ + fields: { + color: rx.bind(colorPickerColor.partial('color')), + } + }), + ]) + ] + + } + }) + }) + }; + + const colorButton = ( + color: ColorDef, isCurrent: rx.Stream, + ): TemplateElementChild => { + return h.div([ + ui5.button( + { + fields: { + className: 'btn-color', + title: color.name, + id: colorHtmlId(color.color), + design: isCurrent.ifelse({ if: 'Default', else: 'Transparent' }), + }, + events: { + click: async _ => { + if (this.editing.value) { + const resp = await openColorPickerDialog(color, true); + const i = palette.value.indexOf(color); + if (resp === 'cancel') return; + if (resp === 'delete') { + palette.next(p => [...p.slice(0, i), ...p.slice(i+1)]) + return; + } + palette.next(p => [...p.slice(0, i), resp, ...p.slice(i+1)]); + } else { + this.dispatch('color-selected', color); + this.popoverRef.current.open = false; + } + } + } + }, [ + h.span( + { + fields: { className: 'btn-color-color' }, + style: { + color: color.color, + backgroundColor: color.color, + }, + }, + ), + h.span( + { + fields: { className: 'btn-color-name' }, + style: { + color: this.editing.ifelse({ + if: ui5.Theme.Button_Attention_TextColor, + else: ui5.Theme.Button_TextColor, + }) + }, + }, + color.name + ), + ], + ), + ]); + } + + return [ + ui5.popover( + { + ref: this.popoverRef, + fields: { + headerText: 'Color', placementType, + id: 'popover', + initialFocus: + currentColor.derive(cc => cc === undefined ? '' : colorHtmlId(cc)), + }, + events: { + 'after-close': e => { + this.editing.next(false); + this.reDispatch('after-close', e); + } + } + }, + [ + h.section( + { fields: { id: 'palette' } }, + palette.derive(p => p.map(col => colorButton( + col, currentColor.derive(cc => cc === col.color) + ))), + ), + h.section( + { slot: 'footer', fields: { id: 'footer-buttons' } }, + [ + ui5.button({ + fields: { icon: 'edit', design: this.editing.ifelse({ + if: 'Attention', else: 'Default' + })}, + style: { marginRight: '.3em' }, + events: { + click: _ => this.editing.next(e => !e), + } + }, this.editing.ifelse({if: 'Finish Edits', else: 'Edit'})), + ui5.button({ + fields: { icon: 'add' }, + events: { + click: async _ => { + this.editing.next(false); + const resp = + await openColorPickerDialog({ name: '', color: '#000000' }, false); + if (resp === 'cancel' || resp === 'delete') return; + palette.next(p => [...p, resp]); + }, + } + }, 'Add Color'), + ] + ) + ] + ), + + ]; + } + + static styles = style.sheet({ + '#popover': { + width: '18em !important', + }, + '#palette': { + display: 'grid', + gridTemplateColumns: '8em 8em', + justifyContent: 'center', + }, + '#footer-buttons': { + display: 'flex', + justifyContent: 'flex-end', + width: '100%', + alignItems: 'center', + padding: '0.5rem 0', + }, + '.btn-color': { + height: '3em', + }, + '.btn-color-color': { + margin: '.3em', + height: '2em', + width: '2em', + borderRadius: '50%', + border: `1px solid ${ui5.Theme.Button_BorderColor}`, + background: 'transparent', + filter: theme.invert, + display: 'inline-block', + verticalAlign: 'middle', + }, + '.btn-color-name': { + display: 'inline-block', + verticalAlign: 'middle', + marginLeft: '.3em', + }, + }); + +} diff --git a/src/renderer/common/dialog-manager.ts b/src/renderer/common/dialog-manager.ts index 89fbff9..0af5af1 100644 --- a/src/renderer/common/dialog-manager.ts +++ b/src/renderer/common/dialog-manager.ts @@ -10,7 +10,7 @@ const LOG = getLogger(__filename); type ButtonDesign = 'Default' | 'Positive' | 'Negative' | 'Transparent' | 'Emphasized' | 'Attention'; -type DialogButtons = { +export type DialogButtons = { name: string, action: () => void, design?: ButtonDesign, icon?: string, }[]; @@ -21,6 +21,7 @@ export type OpenDialog = (decl: (close: () => void) => { buttons: DialogButtons, state?: ui5.types.Dialog['state'], maxWidth?: string, + onCloseNoBtn?: () => void, }) => void; export const BasicDialogManagerContext = new rx.Context<{ @@ -57,7 +58,7 @@ function newDialogId(): number { return DIALOG_COUNTER++; } @Component.register export class BasicDialog extends Component<{ slots: { default: any }, - events: { close: CustomEvent } + events: { close: CustomEvent<{ escPressed: boolean }> } }> { props = { heading: rx.prop(), @@ -115,9 +116,7 @@ export class BasicDialog extends Component<{ ), }, events: { - 'after-close': _ => { - this.dispatch('close', new CustomEvent('close')); - } + 'before-close': e => { this.dispatch('close', e.detail) } } }, h.slot()) ] @@ -149,15 +148,18 @@ export function mkDialogManagerCtx() { buttons: DialogButtons, state?: ui5.types.Dialog['state'], maxWidth?: string, + onCloseNoBtn?: () => void, } ) => { const num = newDialogId(); const close = mkCloseDialog(num); - const { heading, content, buttons, state, maxWidth } = decl(close); + const { heading, content, buttons, state, maxWidth, onCloseNoBtn } = decl(close); LOG.info(`Opening dialogue with heading '${heading}'`); const dialog = BasicDialog.t({ props: { heading, buttons, num, state, maxWidth }, - events: { close } + events: { close: e => { + if (e.detail.escPressed) onCloseNoBtn(); + }} }, content); dialogs.next(d => [...d, dialog]); } diff --git a/src/renderer/document/CanvasSelectionButtons.ts b/src/renderer/document/CanvasSelectionButtons.ts index 83a5e58..705fb56 100644 --- a/src/renderer/document/CanvasSelectionButtons.ts +++ b/src/renderer/document/CanvasSelectionButtons.ts @@ -8,6 +8,7 @@ export default class CanvasSelectionButtons extends Component { private onCut: () => void, private onCopy: () => void, private onDelete: () => void, + // private onChangeColor: (color: string) => void, ) { super(); @@ -41,6 +42,13 @@ export default class CanvasSelectionButtons extends Component { }, // 'Delete' ), + ui5.button( + { + fields: { design: 'Transparent', icon: 'palette', title: 'Change Color' }, + events: { click: this.onDelete } + }, + // 'Change Color' + ), ]) ] } diff --git a/src/renderer/global-styles.ts b/src/renderer/global-styles.ts index 7d54fef..da6ed9a 100644 --- a/src/renderer/global-styles.ts +++ b/src/renderer/global-styles.ts @@ -50,7 +50,7 @@ export const customScrollbar: { [selector: string]: style.MvuiCSSDeclarations } '*::-webkit-scrollbar-corner': { // background: ui5.Theme.ScrollBar_TrackColor, background: 'transparent', - }, + } } style.util.applySheetAsStyleTag(document.body, style.sheet({ diff --git a/src/renderer/persistence/ConfigDTO.ts b/src/renderer/persistence/ConfigDTO.ts index c9614d3..7f313f7 100644 --- a/src/renderer/persistence/ConfigDTO.ts +++ b/src/renderer/persistence/ConfigDTO.ts @@ -121,11 +121,18 @@ const ConfigDTOSchema = { ] }, invertDocument: { type: 'boolean' }, penCursorLeftAngle: { type: 'boolean' }, - colorPalette: { + colorPaletteWrite: { elements: { properties: { color: { type: 'string' }, name: { type: 'string' } } } }, + colorPaletteWriteFavorites: { elements: { type: 'int32' } }, + colorPaletteHighlight: { + elements: { + properties: { color: { type: 'string' }, name: { type: 'string' } } + } + }, + colorPaletteHighlightFavorites: { elements: { type: 'int32' } }, binds: { properties: { rightClick: { enum: CanvasToolNames }, @@ -263,7 +270,7 @@ export function defaultConfig(): ConfigDTO { rightClick: "CanvasToolEraser", middleClick: "CanvasToolHand", }, - colorPalette: [ // stolen from xournal + colorPaletteWrite: [ // stolen from xournal { name: "Black", color: "#000000" }, { name: "Blue", color: "#2F2FE7" }, { name: "Red", color: "#FF0000" }, @@ -276,6 +283,21 @@ export function defaultConfig(): ConfigDTO { { name: "Yellow", color: "#FFFF00" }, { name: "White", color: "#FFFFFF" }, ], + colorPaletteWriteFavorites: [ 0, 1, 2 ], + colorPaletteHighlight: [ + { name: "Yellow", color: "#FFFF00" }, + { name: "Green", color: "#008A00" }, + { name: "Blue", color: "#2F2FE7" }, + { name: "Red", color: "#FF0000" }, + { name: "Magenta", color: "#FF00FF" }, + { name: "Orange", color: "#FF7B00" }, + { name: "Light Blue", color: "#00CAFF" }, + { name: "Light Green", color: "#00FF00" }, + { name: "Black", color: "#000000" }, + { name: "Gray", color: "#808080" }, + { name: "White", color: "#FFFFFF" }, + ], + colorPaletteHighlightFavorites: [ 0, 1, 2 ], autoOpenWojWithSameNameAsPDF: true, hideAnnotations: false, autosave: { diff --git a/src/renderer/util/DOMUtils.ts b/src/renderer/util/DOMUtils.ts index 5d9517d..b64bb37 100644 --- a/src/renderer/util/DOMUtils.ts +++ b/src/renderer/util/DOMUtils.ts @@ -58,5 +58,20 @@ export const DOMUtils = { WOURNAL_SVG_PAGE_PDF_ATTR, ], }); - } + }, + + + /** + * Get the active element (like document.activeElement, except it "pierces" + * shadow roots). Thanks to + * https://www.abeautifulsite.net/posts/finding-the-active-element-in-a-shadow-root/ + */ + getActiveElement: function (root: Document | ShadowRoot = document): Element | null { + const activeEl = root.activeElement; + if (!activeEl) return null; + return (activeEl.shadowRoot) + ? this.getActiveElement(activeEl.shadowRoot) + : activeEl; + }, + } diff --git a/src/renderer/wournal.ts b/src/renderer/wournal.ts index 21548ef..499f7f8 100644 --- a/src/renderer/wournal.ts +++ b/src/renderer/wournal.ts @@ -441,7 +441,7 @@ export default class Wournal extends Component { }, setColorByName: (name: string) => { this.currDoc.value.setColor( - this.configCtx.value.colorPalette.find(c => c.name === name).color + this.configCtx.value.colorPaletteWrite.find(c => c.name === name).color ); }, setColorByHex: (color: string) => {