diff --git a/src/renderer/src/assets/icons/font_size.svg b/src/renderer/src/assets/icons/font_size.svg
new file mode 100644
index 000000000..b80a6bdce
--- /dev/null
+++ b/src/renderer/src/assets/icons/font_size.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/renderer/src/components/DiagramContextMenu.tsx b/src/renderer/src/components/DiagramContextMenu.tsx
deleted file mode 100644
index cfc182aed..000000000
--- a/src/renderer/src/components/DiagramContextMenu.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import React, { Fragment, useLayoutEffect, useState } from 'react';
-
-import { useFloating, offset, flip, shift } from '@floating-ui/react';
-import { twMerge } from 'tailwind-merge';
-
-import { ReactComponent as InitialIcon } from '@renderer/assets/icons/arrow_down_right.svg';
-import { ReactComponent as CameraIcon } from '@renderer/assets/icons/center_focus_2.svg';
-import { ReactComponent as ChoiceStateIcon } from '@renderer/assets/icons/choice_state.svg';
-import { ReactComponent as CodeAllIcon } from '@renderer/assets/icons/code_all_2.svg';
-import { ReactComponent as CopyIcon } from '@renderer/assets/icons/copy.svg';
-import { ReactComponent as DeleteIcon } from '@renderer/assets/icons/delete.svg';
-import { ReactComponent as EditIcon } from '@renderer/assets/icons/edit.svg';
-import { ReactComponent as EventIcon } from '@renderer/assets/icons/event_add.svg';
-import { ReactComponent as FinalStateIcon } from '@renderer/assets/icons/final_state.svg';
-import { ReactComponent as NoteIcon } from '@renderer/assets/icons/note.svg';
-import { ReactComponent as PasteIcon } from '@renderer/assets/icons/paste.svg';
-import { ReactComponent as StateIcon } from '@renderer/assets/icons/state_add.svg';
-import { useClickOutside } from '@renderer/hooks/useClickOutside';
-import { useDiagramContextMenu } from '@renderer/hooks/useDiagramContextMenu';
-import { getVirtualElement } from '@renderer/utils';
-
-const contextData = {
- copy: {
- icon: ,
- combination: 'Ctrl+C',
- },
- paste: {
- icon: ,
- combination: 'Ctrl+V',
- },
- pasteState: {
- icon: ,
- combination: undefined,
- },
- pasteFinalState: {
- icon: ,
- combination: undefined,
- },
- pasteChoiceState: {
- icon: ,
- combination: undefined,
- },
- pasteEvent: {
- icon: ,
- combination: undefined,
- },
- initialState: {
- icon: ,
- combination: undefined,
- },
- showCodeAll: {
- icon: ,
- combination: undefined,
- },
- edit: {
- icon: ,
- combination: undefined,
- },
- centerCamera: {
- icon: ,
- combination: undefined,
- },
- delete: {
- icon: ,
- combination: 'Del',
- },
- source: {
- icon: undefined,
- combination: undefined,
- },
- target: {
- icon: undefined,
- combination: undefined,
- },
- note: {
- icon: ,
- combination: undefined,
- },
-};
-
-export const DiagramContextMenu: React.FC = () => {
- const { position, items, isOpen, onClose } = useDiagramContextMenu();
- //Проверка на открытие дополнительных окон, пока реализовал таким методом, чтобы проверить и распределить данные как следует
- const [openMenu, setOpenMenu] = useState('');
-
- const { refs, floatingStyles } = useFloating({
- placement: 'bottom',
- middleware: [offset(), flip(), shift({ padding: 5 })],
- });
-
- useClickOutside(refs.floating.current, onClose, !isOpen);
-
- // TODO(bryzZz) Два эффекта просто на синхронизацию разных состояний, нужно переделать на один источник истины
- useLayoutEffect(() => {
- refs.setPositionReference(getVirtualElement(position));
- }, [position, refs]);
-
- useLayoutEffect(() => {
- setOpenMenu('');
- }, [isOpen]);
-
- return (
-
- {items.map(({ label, type, isFolder, children, action }, i) => (
-
- {
- action();
- isFolder || onClose();
- }}
- onMouseOver={() => {
- openMenu !== type && setOpenMenu(type);
- }}
- >
-
-
- {contextData[type].icon}
-
-
{label}
-
-
- {contextData[type].combination}
- {isFolder &&
{'>'}
}
-
-
- {openMenu === type && isFolder && (
- //Крайняя мера, которую я не хотел добавлять сюда, я про стили и про дублирующий код
-
- {children &&
- children.map(({ label, action }, i) => (
-
{
- action();
- onClose();
- }}
- >
-
-
- ))}
-
- )}
-
- ))}
-
- );
-};
diff --git a/src/renderer/src/components/DiagramContextMenu/ContextMenu.tsx b/src/renderer/src/components/DiagramContextMenu/ContextMenu.tsx
new file mode 100644
index 000000000..2e19eded1
--- /dev/null
+++ b/src/renderer/src/components/DiagramContextMenu/ContextMenu.tsx
@@ -0,0 +1,66 @@
+import React, { ComponentProps } from 'react';
+
+import { twMerge } from 'tailwind-merge';
+
+import { ContextMenuContext, useContextMenuContext } from './ContextMenuContext';
+
+interface ContextMenuProps {
+ children: React.ReactNode;
+ onClose: () => void;
+}
+
+export const ContextMenu: React.FC = ({ children, onClose }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+interface MenuItemProps extends ComponentProps<'div'> {
+ closeable?: boolean;
+}
+export const MenuItem: React.FC = ({
+ closeable = true,
+ className,
+ onClick,
+ ...props
+}) => {
+ const { onClose } = useContextMenuContext();
+
+ return (
+ {
+ onClick?.(e);
+
+ if (closeable) onClose();
+ }}
+ {...props}
+ />
+ );
+};
+
+type SubMenuContainerProps = ComponentProps<'div'>;
+export const SubMenuContainer: React.FC
= ({ className, ...props }) => {
+ return
;
+};
+
+interface SubMenuProps extends ComponentProps<'div'> {
+ position: 'left' | 'right';
+}
+export const SubMenu: React.FC = ({ className, position, ...props }) => {
+ return (
+
+ );
+};
diff --git a/src/renderer/src/components/DiagramContextMenu/ContextMenuContext.ts b/src/renderer/src/components/DiagramContextMenu/ContextMenuContext.ts
new file mode 100644
index 000000000..c577a5f38
--- /dev/null
+++ b/src/renderer/src/components/DiagramContextMenu/ContextMenuContext.ts
@@ -0,0 +1,13 @@
+import { createContext, useContext } from 'react';
+
+export const ContextMenuContext = createContext<{ onClose: () => void } | null>(null);
+
+export const useContextMenuContext = () => {
+ const value = useContext(ContextMenuContext);
+
+ if (value === null) {
+ throw new Error('There must be a value!');
+ }
+
+ return value;
+};
diff --git a/src/renderer/src/components/DiagramContextMenu/DiagramContextMenu.tsx b/src/renderer/src/components/DiagramContextMenu/DiagramContextMenu.tsx
new file mode 100644
index 000000000..da1193b3f
--- /dev/null
+++ b/src/renderer/src/components/DiagramContextMenu/DiagramContextMenu.tsx
@@ -0,0 +1,426 @@
+import React, { useEffect, useMemo, useState } from 'react';
+
+import { useFloating, offset, flip, shift } from '@floating-ui/react';
+import { twMerge } from 'tailwind-merge';
+
+import { ReactComponent as InitialIcon } from '@renderer/assets/icons/arrow_down_right.svg';
+import { ReactComponent as CameraIcon } from '@renderer/assets/icons/center_focus_2.svg';
+import { ReactComponent as ChoiceStateIcon } from '@renderer/assets/icons/choice_state.svg';
+import { ReactComponent as CodeAllIcon } from '@renderer/assets/icons/code_all_2.svg';
+import { ReactComponent as CopyIcon } from '@renderer/assets/icons/copy.svg';
+import { ReactComponent as DeleteIcon } from '@renderer/assets/icons/delete.svg';
+import { ReactComponent as EditIcon } from '@renderer/assets/icons/edit.svg';
+import { ReactComponent as FinalStateIcon } from '@renderer/assets/icons/final_state.svg';
+import { ReactComponent as NoteIcon } from '@renderer/assets/icons/note.svg';
+import { ReactComponent as PasteIcon } from '@renderer/assets/icons/paste.svg';
+import { ReactComponent as StateIcon } from '@renderer/assets/icons/state_add.svg';
+import { useModal } from '@renderer/hooks';
+import { useClickOutside } from '@renderer/hooks/useClickOutside';
+import {
+ Note,
+ ChoiceState,
+ EventSelection,
+ FinalState,
+ State,
+ Transition,
+} from '@renderer/lib/drawable';
+import { Point } from '@renderer/lib/types';
+import { useEditorContext } from '@renderer/store/EditorContext';
+import { useTabs } from '@renderer/store/useTabs';
+import { getVirtualElement } from '@renderer/utils';
+
+import { ContextMenu, MenuItem, SubMenuContainer, SubMenu } from './ContextMenu';
+import { NoteMenu } from './Menus/NoteMenu';
+
+type MenuVariant =
+ | { type: 'view'; position: Point }
+ | { type: 'state'; state: State; position: Point }
+ | { type: 'finalState'; state: FinalState }
+ | { type: 'choiceState'; state: ChoiceState }
+ | { type: 'event'; state: State; event: EventSelection }
+ | { type: 'transition'; transition: Transition; position: Point }
+ | { type: 'note'; note: Note; position: Point };
+
+export const DiagramContextMenu: React.FC = () => {
+ const editor = useEditorContext();
+
+ const openTab = useTabs((state) => state.openTab);
+
+ const [isOpen, open, close] = useModal(false);
+ const [menuVariant, setMenuVariant] = useState(null);
+
+ const { refs, floatingStyles } = useFloating({
+ placement: 'bottom',
+ middleware: [offset(), flip(), shift({ padding: 5 })],
+ });
+
+ useClickOutside(refs.floating.current, close, !isOpen, '#color-picker');
+
+ useEffect(() => {
+ const handleEvent = (menuVariant: MenuVariant, position: Point) => {
+ refs.setPositionReference(getVirtualElement(position));
+ setMenuVariant(menuVariant);
+ open();
+ };
+
+ const handleViewContextMenu = (position: Point) => {
+ handleEvent({ type: 'view', position }, position);
+ };
+ const handleStateContextMenu = ({ state, position }: { state: State; position: Point }) => {
+ handleEvent({ type: 'state', state, position }, position);
+ };
+ const handleFinalStateContextMenu = (data: { state: FinalState; position: Point }) => {
+ const { state, position } = data;
+
+ handleEvent({ type: 'finalState', state }, position);
+ };
+ const handleChoiceStateContextMenu = (data: { state: ChoiceState; position: Point }) => {
+ const { state, position } = data;
+
+ handleEvent({ type: 'choiceState', state }, position);
+ };
+ const handleEventContextMenu = (data: {
+ state: State;
+ position: Point;
+ event: EventSelection;
+ }) => {
+ const { state, position, event } = data;
+
+ handleEvent({ type: 'event', state, event }, position);
+ };
+ const handleTransitionContextMenu = (data: { transition: Transition; position: Point }) => {
+ const { transition, position } = data;
+
+ handleEvent({ type: 'transition', transition, position }, position);
+ };
+ const handleNoteContextMenu = ({ position, note }: { position: Point; note: Note }) => {
+ handleEvent({ type: 'note', note, position }, position);
+ };
+
+ // контекстное меню для пустого поля
+ editor.view.on('contextMenu', handleViewContextMenu);
+ // контекстное меню для состояний
+ editor.controller.states.on('stateContextMenu', handleStateContextMenu);
+ editor.controller.states.on('finalStateContextMenu', handleFinalStateContextMenu);
+ editor.controller.states.on('choiceStateContextMenu', handleChoiceStateContextMenu);
+ // контекстное меню для события
+ editor.controller.states.on('eventContextMenu', handleEventContextMenu);
+ // контекстное меню для связи
+ editor.controller.transitions.on('transitionContextMenu', handleTransitionContextMenu);
+ editor.controller.notes.on('contextMenu', handleNoteContextMenu);
+
+ //! Не забывать удалять слушатели
+ return () => {
+ editor.view.off('contextMenu', handleViewContextMenu);
+ editor.controller.states.off('stateContextMenu', handleStateContextMenu);
+ editor.controller.states.off('finalStateContextMenu', handleFinalStateContextMenu);
+ editor.controller.states.off('choiceStateContextMenu', handleChoiceStateContextMenu);
+ editor.controller.states.off('eventContextMenu', handleEventContextMenu);
+ editor.controller.transitions.off('transitionContextMenu', handleTransitionContextMenu);
+ editor.controller.notes.off('contextMenu', handleNoteContextMenu);
+ };
+ }, [editor, open, refs]);
+
+ const content = useMemo(() => {
+ if (!menuVariant) return null;
+
+ if (menuVariant.type === 'view') {
+ const { position } = menuVariant;
+
+ const mouseOffset = editor.view.app.mouse.getOffset();
+ const canvasPos = editor.view.windowToWorldCoords({
+ x: position.x - mouseOffset.x,
+ y: position.y - mouseOffset.y,
+ });
+
+ return (
+
+ editor.controller.pasteSelected()}>
+ Вставить
+ Ctrl+V
+
+
+ editor.controller.states.createState({
+ name: 'Состояние',
+ position: canvasPos,
+ placeInCenter: true,
+ })
+ }
+ >
+ Вставить состояние
+
+
+ editor.controller.states.createFinalState({
+ position: canvasPos,
+ placeInCenter: true,
+ })
+ }
+ >
+ Вставить конечное состояние
+
+
+ editor.controller.states.createChoiceState({
+ position: canvasPos,
+ placeInCenter: true,
+ })
+ }
+ >
+ Вставить состояние выбора
+
+ {
+ const note = editor.controller.notes.createNote({
+ position: canvasPos,
+ placeInCenter: true,
+ text: '',
+ });
+
+ editor.controller.notes.emit('change', note);
+ }}
+ >
+ Вставить заметку
+
+
+ openTab({
+ type: 'code',
+ name: editor.model.data.name ?? 'Безымянная',
+ code: editor.model.serializer.getAll('JSON'),
+ language: 'json',
+ })
+ }
+ >
+ Посмотреть код
+
+ editor.view.viewCentering()}>
+ Центрировать камеру
+
+
+ );
+ }
+
+ if (menuVariant.type === 'state') {
+ const { state, position } = menuVariant;
+
+ return (
+
+ editor.controller.copySelected()}>
+ Копировать
+ Ctrl+C
+
+
+ editor.controller.pasteSelected()}>
+ Вставить
+ Ctrl+V
+
+
+
+
+ Редактировать
+ {'>'}
+
+
+
+ editor.controller.states.setInitialState(state.id)}>
+
+ Назначить начальным
+
+
+ editor.controller.states.createState({
+ name: 'Состояние',
+ position: editor.view.windowToWorldCoords(position),
+ parentId: state.id,
+ })
+ }
+ >
+
+ Вставить состояние
+
+
+
+
+
+ openTab({
+ type: 'state',
+ name: state.data.name,
+ code: editor.model.serializer.getState(state.id) ?? '',
+ language: 'json',
+ })
+ }
+ >
+ Посмотреть код
+
+
+ editor.controller.states.deleteState(state.id)}
+ >
+ Удалить
+ Del
+
+
+ );
+ }
+
+ if (menuVariant.type === 'finalState') {
+ const { state } = menuVariant;
+ return (
+
+ editor.controller.states.deleteFinalState(state.id)}
+ >
+ Удалить
+ Del
+
+
+ );
+ }
+
+ if (menuVariant.type === 'choiceState') {
+ const { state } = menuVariant;
+
+ return (
+
+ editor.controller.states.deleteChoiceState(state.id)}
+ >
+ Удалить
+ Del
+
+
+ );
+ }
+
+ if (menuVariant.type === 'event') {
+ const { state, event } = menuVariant;
+
+ return (
+
+ editor.controller.states.deleteEvent(state.id, event)}
+ >
+ Удалить
+ Del
+
+
+ );
+ }
+
+ if (menuVariant.type === 'transition') {
+ const { transition, position } = menuVariant;
+
+ const sourceArray = Array.from(editor.controller.states.getStates()).filter(
+ (value) => transition.data.sourceId !== value[0]
+ );
+ const targetArray = Array.from(editor.controller.states.getStates()).filter(
+ (value) => transition.data.targetId !== value[0]
+ );
+
+ return (
+
+ editor.controller.copySelected()}>
+ Копировать
+ Ctrl+C
+
+
+
+
+ Выбрать исход(source)
+ {'>'}
+
+
+
+ {sourceArray.map(([id, state]) => (
+
+ editor.controller.transitions.changeTransition({
+ ...transition.data,
+ id: transition.id,
+ sourceId: id,
+ })
+ }
+ >
+
+ {state.data.name}
+
+ ))}
+
+
+
+
+
+ Выбрать цель(target)
+ {'>'}
+
+
+
+ {targetArray.map(([id, state]) => (
+
+ editor.controller.transitions.changeTransition({
+ ...transition.data,
+ id: transition.id,
+ targetId: id,
+ })
+ }
+ >
+
+ {state.data.name}
+
+ ))}
+
+
+
+
+ openTab({
+ type: 'transition',
+ name: transition.id,
+ code: editor.model.serializer.getTransition(transition.id) ?? '',
+ language: 'json',
+ })
+ }
+ >
+ Посмотреть код
+
+
+ editor.controller.transitions.deleteTransition(transition.id)}
+ >
+ Удалить
+ Del
+
+
+ );
+ }
+
+ if (menuVariant.type === 'note') {
+ const { note, position } = menuVariant;
+
+ return ;
+ }
+
+ return null;
+ }, [close, editor, menuVariant, openTab]);
+
+ return (
+
+ {content}
+
+ );
+};
diff --git a/src/renderer/src/components/DiagramContextMenu/Menus/NoteMenu.tsx b/src/renderer/src/components/DiagramContextMenu/Menus/NoteMenu.tsx
new file mode 100644
index 000000000..e0c0aa562
--- /dev/null
+++ b/src/renderer/src/components/DiagramContextMenu/Menus/NoteMenu.tsx
@@ -0,0 +1,98 @@
+import React, { useLayoutEffect, useState } from 'react';
+
+import { ReactComponent as CheckIcon } from '@renderer/assets/icons/check.svg';
+import { ReactComponent as DeleteIcon } from '@renderer/assets/icons/delete.svg';
+import { ReactComponent as EditIcon } from '@renderer/assets/icons/edit.svg';
+import { ReactComponent as FontSizeIcon } from '@renderer/assets/icons/font_size.svg';
+import { ColorInput } from '@renderer/components/UI';
+import { Note } from '@renderer/lib/drawable';
+import { Point } from '@renderer/lib/types';
+import { useEditorContext } from '@renderer/store/EditorContext';
+
+import { ContextMenu, MenuItem, SubMenu, SubMenuContainer } from '../ContextMenu';
+
+interface NoteMenuProps {
+ position: Point;
+ note: Note;
+ onClose: () => void;
+}
+
+const fontSizes = [12, 14, 16, 18, 20, 22];
+
+export const NoteMenu: React.FC = ({ onClose, note, position }) => {
+ const editor = useEditorContext();
+
+ const [bgColor, setBgColor] = useState(undefined);
+ const [textColor, setTextColor] = useState(undefined);
+
+ const handleBgColorPickerClose = () => {
+ if (note.data?.backgroundColor !== bgColor) {
+ editor.controller.notes.changeNoteBackgroundColor(note.id, bgColor);
+ }
+ };
+
+ const handleTextColorPickerClose = () => {
+ if (note.data?.textColor !== textColor) {
+ editor.controller.notes.changeNoteTextColor(note.id, textColor);
+ }
+ };
+
+ // Выставление начальных данных
+ useLayoutEffect(() => {
+ setBgColor(note.data?.backgroundColor);
+ setTextColor(note.data?.textColor);
+ }, [note.data?.backgroundColor, note.data?.textColor]);
+
+ return (
+
+ editor.controller.notes.emit('change', note)}>
+ Редактировать
+
+
+ Цвет фона
+
+
+
+ Цвет текста
+
+
+
+
+
+ Размер шрифта
+
+
+
+ {fontSizes.map((size) => (
+ editor.controller.notes.changeNoteFontSize(note.id, size)}
+ >
+ {size}px
+ {(note.data?.fontSize ?? 16) === size && (
+
+ )}
+
+ ))}
+
+
+ editor.controller.notes.deleteNote(note.id)}
+ >
+ Удалить
+ Del
+
+
+ );
+};
diff --git a/src/renderer/src/components/DiagramContextMenu/index.ts b/src/renderer/src/components/DiagramContextMenu/index.ts
new file mode 100644
index 000000000..021531124
--- /dev/null
+++ b/src/renderer/src/components/DiagramContextMenu/index.ts
@@ -0,0 +1 @@
+export * from './DiagramContextMenu';
diff --git a/src/renderer/src/components/NoteEdit.tsx b/src/renderer/src/components/NoteEdit.tsx
index bc69564d5..f3e01bf31 100644
--- a/src/renderer/src/components/NoteEdit.tsx
+++ b/src/renderer/src/components/NoteEdit.tsx
@@ -68,6 +68,8 @@ export const NoteEdit: React.FC = () => {
fontSize: fontSize + 'px',
padding: padding + 'px',
borderRadius: borderRadius + 'px',
+ backgroundColor: note.data?.backgroundColor,
+ color: note.data?.textColor,
});
el.textContent = note.data.text;
setTimeout(() => placeCaretAtEnd(el), 0); // А ты думал легко сфокусировать и установить картеку в конец?
@@ -87,7 +89,7 @@ export const NoteEdit: React.FC = () => {
tabIndex={-1}
style={style}
className={twMerge(
- 'fixed overflow-hidden whitespace-pre-wrap border-none bg-bg-secondary text-base leading-none outline outline-1 outline-text-primary',
+ 'fixed overflow-hidden whitespace-pre-wrap border-none bg-bg-secondary text-base leading-[1.2] outline outline-1 outline-text-primary',
!isOpen && 'hidden'
)}
placeholder="Придумайте заметку"
diff --git a/src/renderer/src/hooks/useClickOutside.ts b/src/renderer/src/hooks/useClickOutside.ts
index 80141f901..e45a290b4 100644
--- a/src/renderer/src/hooks/useClickOutside.ts
+++ b/src/renderer/src/hooks/useClickOutside.ts
@@ -4,14 +4,27 @@ export const useClickOutside = (
element: HTMLElement | null,
action: () => void,
disabled = false,
- additionalElement: HTMLElement | null = null
+ additionalElement: HTMLElement | string | null = null
) => {
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (disabled || !element) return;
if (element.contains(e.target as HTMLElement)) return;
- if (additionalElement && additionalElement.contains(e.target as HTMLElement)) return;
+ if (additionalElement) {
+ const element =
+ typeof additionalElement === 'string'
+ ? document.querySelectorAll(additionalElement)
+ : additionalElement;
+
+ if (
+ element &&
+ ((element instanceof HTMLElement && element.contains(e.target as HTMLElement)) ||
+ (element instanceof NodeList &&
+ [...element.values()].some((el) => el.contains(e.target as HTMLElement))))
+ )
+ return;
+ }
action();
};
diff --git a/src/renderer/src/lib/data/EditorController/NotesController.ts b/src/renderer/src/lib/data/EditorController/NotesController.ts
index 5a5d998ae..5ad7c81aa 100644
--- a/src/renderer/src/lib/data/EditorController/NotesController.ts
+++ b/src/renderer/src/lib/data/EditorController/NotesController.ts
@@ -78,6 +78,55 @@ export class NotesController extends EventEmitter {
this.view.isDirty = true;
};
+ changeNoteBackgroundColor = (id: string, color: string | undefined, canUndo = true) => {
+ const note = this.items.get(id);
+ if (!note) return;
+
+ if (canUndo) {
+ this.history.do({
+ type: 'changeNoteBackgroundColor',
+ args: { id, color, prevColor: note.data?.backgroundColor },
+ });
+ }
+
+ this.app.model.changeNoteBackgroundColor(id, color);
+
+ this.view.isDirty = true;
+ };
+
+ changeNoteTextColor = (id: string, color: string | undefined, canUndo = true) => {
+ const note = this.items.get(id);
+ if (!note) return;
+
+ if (canUndo) {
+ this.history.do({
+ type: 'changeNoteTextColor',
+ args: { id, color, prevColor: note.data?.textColor },
+ });
+ }
+
+ this.app.model.changeNoteTextColor(id, color);
+
+ this.view.isDirty = true;
+ };
+
+ changeNoteFontSize = (id: string, fontSize: number | undefined, canUndo = true) => {
+ const note = this.items.get(id);
+ if (!note) return;
+
+ if (canUndo) {
+ this.history.do({
+ type: 'changeNoteFontSize',
+ args: { id, fontSize, prevFontSize: note.data?.fontSize },
+ });
+ }
+
+ this.app.model.changeNoteFontSize(id, fontSize);
+ note.prepareText();
+
+ this.view.isDirty = true;
+ };
+
changeNotePosition(id: string, startPosition: Point, endPosition: Point, canUndo = true) {
const note = this.items.get(id);
if (!note) return;
diff --git a/src/renderer/src/lib/data/EditorModel/EditorModel.ts b/src/renderer/src/lib/data/EditorModel/EditorModel.ts
index 031df1f89..017fc6fd3 100644
--- a/src/renderer/src/lib/data/EditorModel/EditorModel.ts
+++ b/src/renderer/src/lib/data/EditorModel/EditorModel.ts
@@ -713,6 +713,36 @@ export class EditorModel {
return true;
}
+ changeNoteBackgroundColor(id: string, color: string | undefined) {
+ if (!this.data.elements.notes.hasOwnProperty(id)) return false;
+
+ this.data.elements.notes[id].backgroundColor = color;
+
+ this.triggerDataUpdate('elements.notes');
+
+ return true;
+ }
+
+ changeNoteTextColor(id: string, color: string | undefined) {
+ if (!this.data.elements.notes.hasOwnProperty(id)) return false;
+
+ this.data.elements.notes[id].textColor = color;
+
+ this.triggerDataUpdate('elements.notes');
+
+ return true;
+ }
+
+ changeNoteFontSize(id: string, fontSize: number | undefined) {
+ if (!this.data.elements.notes.hasOwnProperty(id)) return false;
+
+ this.data.elements.notes[id].fontSize = fontSize;
+
+ this.triggerDataUpdate('elements.notes');
+
+ return true;
+ }
+
//TODO: (XidFanSan) Выделение пока будет так работать, в дальнейшем требуется доработка
changeNoteSelection(id: string, selection: boolean) {
const note = this.data.elements.notes[id];
diff --git a/src/renderer/src/lib/data/History.ts b/src/renderer/src/lib/data/History.ts
index 0c871e785..9ae760f41 100644
--- a/src/renderer/src/lib/data/History.ts
+++ b/src/renderer/src/lib/data/History.ts
@@ -81,6 +81,21 @@ export type PossibleActions = {
createNote: { id: string; params: CreateNoteParams };
changeNotePosition: { id: string; startPosition: Point; endPosition: Point };
changeNoteText: { id: string; text: string; prevText: string };
+ changeNoteBackgroundColor: {
+ id: string;
+ color: string | undefined;
+ prevColor: string | undefined;
+ };
+ changeNoteTextColor: {
+ id: string;
+ color: string | undefined;
+ prevColor: string | undefined;
+ };
+ changeNoteFontSize: {
+ id: string;
+ fontSize: number | undefined;
+ prevFontSize: number | undefined;
+ };
deleteNote: { id: string; prevData: NoteData };
};
export type PossibleActionTypes = keyof PossibleActions;
@@ -346,6 +361,18 @@ export const actionFunctions: ActionFunctions = {
redo: sM.notes.changeNoteText.bind(sM.notes, id, text, false),
undo: sM.notes.changeNoteText.bind(sM.notes, id, prevText, false),
}),
+ changeNoteBackgroundColor: (sM, { id, color, prevColor }) => ({
+ redo: sM.notes.changeNoteBackgroundColor.bind(sM.notes, id, color, false),
+ undo: sM.notes.changeNoteBackgroundColor.bind(sM.notes, id, prevColor, false),
+ }),
+ changeNoteTextColor: (sM, { id, color, prevColor }) => ({
+ redo: sM.notes.changeNoteTextColor.bind(sM.notes, id, color, false),
+ undo: sM.notes.changeNoteTextColor.bind(sM.notes, id, prevColor, false),
+ }),
+ changeNoteFontSize: (sM, { id, fontSize, prevFontSize }) => ({
+ redo: sM.notes.changeNoteFontSize.bind(sM.notes, id, fontSize, false),
+ undo: sM.notes.changeNoteFontSize.bind(sM.notes, id, prevFontSize, false),
+ }),
changeNotePosition: (sM, { id, startPosition, endPosition }) => ({
redo: sM.notes.changeNotePosition.bind(sM.notes, id, startPosition, endPosition, false),
undo: sM.notes.changeNotePosition.bind(sM.notes, id, endPosition, startPosition, false),
@@ -497,6 +524,18 @@ export const actionDescriptions: ActionDescriptions = {
name: 'Изменение текста заметки',
description: `ID: ${args.id}\nБыло: "${args.prevText}"\nСтало: "${args.text}"`,
}),
+ changeNoteBackgroundColor: (args) => ({
+ name: 'Изменение цвета заметки',
+ description: `ID: ${args.id}\nБыло: "${args.prevColor}"\nСтало: "${args.color}"`,
+ }),
+ changeNoteTextColor: (args) => ({
+ name: 'Изменение цвета текста заметки',
+ description: `ID: ${args.id}\nБыло: "${args.prevColor}"\nСтало: "${args.color}"`,
+ }),
+ changeNoteFontSize: (args) => ({
+ name: 'Изменение размера шрифта заметки',
+ description: `ID: ${args.id}\nБыло: "${args.prevFontSize}"\nСтало: "${args.fontSize}"`,
+ }),
changeNotePosition: (args) => ({
name: 'Перемещение заметки',
description: `Id: "${args.id}"\nБыло: ${JSON.stringify(
diff --git a/src/renderer/src/lib/drawable/Note.ts b/src/renderer/src/lib/drawable/Note.ts
index d1802400d..73f8cd8a0 100644
--- a/src/renderer/src/lib/drawable/Note.ts
+++ b/src/renderer/src/lib/drawable/Note.ts
@@ -56,9 +56,11 @@ export class Note extends Shape {
return {
padding: 10 / scale,
- fontSize: 16 / scale,
+ fontSize: (this.data.fontSize ?? 16) / scale,
borderRadius: 6 / scale,
- color: this.textData.hasText ? getColor('text-primary') : getColor('border-primary'),
+ color: this.textData.hasText
+ ? this.data?.textColor ?? getColor('default-note-color')
+ : getColor('border-primary'),
};
}
@@ -76,13 +78,15 @@ export class Note extends Shape {
}
prepareText() {
+ const hasText = Boolean(this.data.text);
+
this.textData = {
...prepareText(this.data.text || placeholder, 200 - 2 * 10, {
- fontSize: 16,
- lineHeight: 1,
+ fontSize: this.data.fontSize ?? 16,
+ lineHeight: hasText ? 1.2 : 1,
fontFamily: 'Fira Sans',
}),
- hasText: Boolean(this.data.text),
+ hasText,
};
}
@@ -93,15 +97,12 @@ export class Note extends Shape {
const textToDraw = this.textData.hasText ? this.textData.textArray : placeholder;
const { padding, fontSize, color, borderRadius } = this.computedStyles;
- ctx.fillStyle = 'black';
- ctx.globalAlpha = 0.3;
+ ctx.fillStyle = this.data.backgroundColor ?? getColor('default-note-bg');
ctx.beginPath();
ctx.roundRect(x, y, width, height, borderRadius);
ctx.fill();
- ctx.globalAlpha = 1;
-
drawText(ctx, textToDraw, {
x: x + padding,
y: y + padding,
@@ -109,7 +110,7 @@ export class Note extends Shape {
color,
font: {
fontSize,
- lineHeight: 1,
+ lineHeight: this.textData.hasText ? 1.2 : 1,
fontFamily: 'Fira Sans',
},
});
diff --git a/src/renderer/src/theme.ts b/src/renderer/src/theme.ts
index 888d7a28c..784d541a5 100644
--- a/src/renderer/src/theme.ts
+++ b/src/renderer/src/theme.ts
@@ -26,6 +26,8 @@ const colorNames = {
'scrollbar-thumb': '--s-th',
grid: '--g',
+ 'default-note-bg': '--d-n-bg',
+ 'default-note-color': '--d-n-c',
'default-transition-color': '--d-t-c',
'default-state-color': '--d-s-c',
} as const;
diff --git a/src/renderer/src/types/diagram.ts b/src/renderer/src/types/diagram.ts
index 75a30f829..c9df461a1 100644
--- a/src/renderer/src/types/diagram.ts
+++ b/src/renderer/src/types/diagram.ts
@@ -86,6 +86,9 @@ export type Component = {
export type Note = {
position: Point;
text: string;
+ backgroundColor?: string;
+ textColor?: string;
+ fontSize?: number;
//TODO: В дальнейшем планируется убрать
selection?: boolean;
};
diff --git a/tailwind.config.js b/tailwind.config.js
index bb8e8414c..b3e1070b7 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -47,6 +47,8 @@ export default {
'--s-th': 'rgba(162,162,162, 0.7)',
'--g': 'rgba(255,255,255,0.03)',
+ '--d-n-bg': 'rgba(0,0,0,0.3)',
+ '--d-n-c': '#FFFFFF',
'--d-t-c': '#F2F2F2',
'--d-s-c': '#F2F2F2',
},
@@ -78,6 +80,8 @@ export default {
'--s-th': 'rgba(162,162,162, 0.7)',
'--g': 'rgba(0,0,0,0.08)',
+ '--d-n-bg': 'rgba(255,255,255,0.5)',
+ '--d-n-c': '#000000',
'--d-t-c': '#404040',
'--d-s-c': '#F2F2F2',
},