Skip to content

Commit

Permalink
wip: color popover
Browse files Browse the repository at this point in the history
  • Loading branch information
dominiksta committed Oct 19, 2024
1 parent 6a5aaa9 commit 97c931c
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 19 deletions.
7 changes: 1 addition & 6 deletions src/renderer/app/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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')),
Expand Down
44 changes: 43 additions & 1 deletion src/renderer/app/toolbars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -88,6 +89,23 @@ export default class Toolbars extends Component {
});
const shapePopoverRef = this.ref<ui5.types.Popover>();

const colorPopoverRef = this.ref<ColorPopover>();
async function pickColor(showAt: HTMLElement): Promise<'cancel' | ColorDef> {
return new Promise(resolve => {
const pop = colorPopoverRef.current;
function onPicked(e: CustomEvent<ColorDef>) { 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
Expand Down Expand Up @@ -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,
Expand All @@ -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',
Expand Down
245 changes: 245 additions & 0 deletions src/renderer/common/color-popover.ts
Original file line number Diff line number Diff line change
@@ -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<ColorDef>,
'after-close': CustomEvent<void>,
},
}> {
props = {
placementType: rx.prop<PopoverPlacementType>({ defaultValue: 'Bottom' }),
palette: rx.prop<ColorPalette>(),
currentColor: rx.prop<string | undefined>(),
}

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<ui5.types.Popover>();
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<ColorDef>({...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<boolean>,
): 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',
},
});

}
16 changes: 9 additions & 7 deletions src/renderer/common/dialog-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}[];
Expand All @@ -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<{
Expand Down Expand Up @@ -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<TemplateElementChild>(),
Expand Down Expand Up @@ -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())
]
Expand Down Expand Up @@ -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]);
}
Expand Down
Loading

0 comments on commit 97c931c

Please sign in to comment.