mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-11-20 17:21:24 +03:00
F: Improve termgraph UI accelerators
This commit is contained in:
parent
f63d9cf93e
commit
584f62579d
|
|
@ -0,0 +1,79 @@
|
|||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
interface PanOptions {
|
||||
panSpeed: number;
|
||||
}
|
||||
|
||||
export function useContinuousPan(
|
||||
ref: React.RefObject<HTMLDivElement | null>,
|
||||
options: PanOptions = {
|
||||
panSpeed: 15
|
||||
}
|
||||
) {
|
||||
const { getViewport, setViewport } = useReactFlow();
|
||||
|
||||
const keysPressed = useRef<Set<string>>(new Set());
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
const panLoop = useCallback(() => {
|
||||
const viewport = getViewport();
|
||||
let { x, y } = viewport;
|
||||
|
||||
if (keysPressed.current.has('KeyW')) y += options.panSpeed;
|
||||
if (keysPressed.current.has('KeyS')) y -= options.panSpeed;
|
||||
if (keysPressed.current.has('KeyA')) x += options.panSpeed;
|
||||
if (keysPressed.current.has('KeyD')) x -= options.panSpeed;
|
||||
|
||||
setViewport({ x, y, zoom: viewport.zoom }, { duration: 0 });
|
||||
// eslint-disable-next-line react-hooks/immutability
|
||||
rafRef.current = requestAnimationFrame(() => panLoop());
|
||||
}, [options.panSpeed, getViewport, setViewport]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
||||
if (!['KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(event.code)) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
keysPressed.current.add(event.code);
|
||||
if (rafRef.current === null) {
|
||||
rafRef.current = requestAnimationFrame(panLoop);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyUp = (event: KeyboardEvent) => {
|
||||
keysPressed.current.delete(event.code);
|
||||
if (keysPressed.current.size === 0 && rafRef.current !== null) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
keysPressed.current.clear();
|
||||
if (rafRef.current !== null) {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener('keydown', handleKeyDown, { passive: false });
|
||||
element.addEventListener('keyup', handleKeyUp);
|
||||
element.addEventListener('blur', handleBlur);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener('keydown', handleKeyDown);
|
||||
element.removeEventListener('keyup', handleKeyUp);
|
||||
element.removeEventListener('blur', handleBlur);
|
||||
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [ref, panLoop]);
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
import { IconEdgeType } from '@/features/rsform/components/icon-edge-type';
|
||||
import { IconGraphMode } from '@/features/rsform/components/icon-graph-mode';
|
||||
import { InteractionMode, TGEdgeType } from '@/features/rsform/stores/term-graph';
|
||||
|
||||
import { Divider } from '@/components/container';
|
||||
import {
|
||||
IconChild,
|
||||
|
|
@ -80,6 +84,9 @@ export function HelpRSGraphTerm() {
|
|||
<li>
|
||||
<kbd>Space</kbd> – перемещение экрана
|
||||
</li>
|
||||
<li>
|
||||
<kbd>WASD</kbd> - направленное перемещение
|
||||
</li>
|
||||
<li>
|
||||
<IconOSS className='inline-icon' /> переход к связанной <LinkTopic text='ОСС' topic={HelpTopic.CC_OSS} />
|
||||
</li>
|
||||
|
|
@ -96,6 +103,18 @@ export function HelpRSGraphTerm() {
|
|||
<IconTypeGraph className='inline-icon' /> Открыть{' '}
|
||||
<LinkTopic text='граф ступеней' topic={HelpTopic.UI_TYPE_GRAPH} />
|
||||
</li>
|
||||
<li>
|
||||
<IconGraphMode value={InteractionMode.explore} className='inline-icon' /> Просмотр графа
|
||||
</li>
|
||||
<li>
|
||||
<IconGraphMode value={InteractionMode.edit} className='inline-icon icon-green' /> Редактирование связей
|
||||
</li>
|
||||
<li>
|
||||
<IconEdgeType value={TGEdgeType.attribution} className='inline-icon' /> Атрибутирование
|
||||
</li>
|
||||
<li>
|
||||
<IconEdgeType value={TGEdgeType.definition} className='inline-icon' /> Определение
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export class RSFormLoader {
|
|||
private schema: IRSForm;
|
||||
private graph: Graph = new Graph();
|
||||
private association_graph: Graph = new Graph();
|
||||
private full_graph: Graph = new Graph();
|
||||
private cstByAlias = new Map<string, IConstituenta>();
|
||||
private cstByID = new Map<number, IConstituenta>();
|
||||
|
||||
|
|
@ -42,7 +41,6 @@ export class RSFormLoader {
|
|||
result.cstByAlias = this.cstByAlias;
|
||||
result.cstByID = this.cstByID;
|
||||
result.attribution_graph = this.association_graph;
|
||||
result.full_graph = this.full_graph;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +50,6 @@ export class RSFormLoader {
|
|||
this.cstByID.set(cst.id, cst);
|
||||
this.graph.addNode(cst.id);
|
||||
this.association_graph.addNode(cst.id);
|
||||
this.full_graph.addNode(cst.id);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +60,6 @@ export class RSFormLoader {
|
|||
const source = this.cstByAlias.get(alias);
|
||||
if (source) {
|
||||
this.graph.addEdge(source.id, cst.id);
|
||||
this.full_graph.addEdge(source.id, cst.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -113,9 +109,6 @@ export class RSFormLoader {
|
|||
this.schema.attribution.forEach(attrib => {
|
||||
const container = this.cstByID.get(attrib.container)!;
|
||||
container.attributes.push(attrib.attribute);
|
||||
if (!this.full_graph.hasEdge(attrib.attribute, attrib.container)) {
|
||||
this.full_graph.addEdge(attrib.container, attrib.attribute);
|
||||
}
|
||||
this.association_graph.addEdge(attrib.container, attrib.attribute);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ export function colorBgCstClass(cstClass: CstClass): string {
|
|||
case CstClass.BASIC: return APP_COLORS.bgGreen;
|
||||
case CstClass.DERIVED: return APP_COLORS.bgBlue;
|
||||
case CstClass.STATEMENT: return APP_COLORS.bgRed;
|
||||
case CstClass.TEMPLATE: return APP_COLORS.bgTeal;
|
||||
case CstClass.TEMPLATE: return APP_COLORS.bgPurple;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export function TermGraphEdge({ id, markerEnd, style, ...props }: EdgeProps) {
|
|||
return (
|
||||
<>
|
||||
<path id={id} className='react-flow__edge-path' d={path} markerEnd={markerEnd} style={style} />
|
||||
<path d={path} className='rf-edge-events cursor-pointer!' />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { type Styling } from '@/components/props';
|
||||
import { cn } from '@/components/utils';
|
||||
import { type Graph } from '@/models/graph';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
interface ToolbarGraphSelectionProps extends Styling {
|
||||
value: number[];
|
||||
|
|
@ -26,12 +27,14 @@ interface ToolbarGraphSelectionProps extends Styling {
|
|||
isCore: (item: number) => boolean;
|
||||
isCrucial: (item: number) => boolean;
|
||||
isInherited: (item: number) => boolean;
|
||||
tipHotkeys?: boolean;
|
||||
}
|
||||
|
||||
export function ToolbarGraphSelection({
|
||||
className,
|
||||
graph,
|
||||
value,
|
||||
tipHotkeys,
|
||||
isCore,
|
||||
isInherited,
|
||||
isCrucial,
|
||||
|
|
@ -110,7 +113,8 @@ export function ToolbarGraphSelection({
|
|||
return (
|
||||
<div className={cn('cc-icons items-center', className)} {...restProps}>
|
||||
<MiniButton
|
||||
title='Сбросить выделение'
|
||||
title={!tipHotkeys ? 'Сбросить выделение' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Сбросить выделение', 'Esc') : undefined}
|
||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectReset}
|
||||
disabled={emptySelection}
|
||||
|
|
@ -127,14 +131,16 @@ export function ToolbarGraphSelection({
|
|||
<Dropdown isOpen={isSelectedOpen} className='-translate-x-1/2'>
|
||||
<DropdownButton
|
||||
text='Поставщики'
|
||||
title='Выделить поставщиков'
|
||||
title={!tipHotkeys ? 'Выделить поставщиков' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить поставщиков', '1') : undefined}
|
||||
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleExpandInputs}
|
||||
disabled={emptySelection}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Потребители'
|
||||
title='Выделить потребителей'
|
||||
title={!tipHotkeys ? 'Выделить потребителей' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить потребителей', '2') : undefined}
|
||||
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleExpandOutputs}
|
||||
disabled={emptySelection}
|
||||
|
|
@ -142,14 +148,16 @@ export function ToolbarGraphSelection({
|
|||
|
||||
<DropdownButton
|
||||
text='Влияющие'
|
||||
title='Выделить все влияющие'
|
||||
title={!tipHotkeys ? 'Выделить все влияющие' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить все влияющие', '3') : undefined}
|
||||
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectAllInputs}
|
||||
disabled={emptySelection}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='Зависимые'
|
||||
title='Выделить все зависимые'
|
||||
title={!tipHotkeys ? 'Выделить все зависимые' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить все зависимые', '4') : undefined}
|
||||
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectAllOutputs}
|
||||
disabled={emptySelection}
|
||||
|
|
@ -157,7 +165,14 @@ export function ToolbarGraphSelection({
|
|||
|
||||
<DropdownButton
|
||||
text='Максимизация'
|
||||
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
||||
titleHtml={
|
||||
!tipHotkeys
|
||||
? '<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
|
||||
: prepareTooltip(
|
||||
'Максимизация - дополнение выделения конституентами, зависимыми только от выделенных',
|
||||
'5'
|
||||
)
|
||||
}
|
||||
aria-label='Максимизация - дополнение выделения конституентами, зависимыми только от выделенных'
|
||||
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectMaximize}
|
||||
|
|
@ -165,6 +180,7 @@ export function ToolbarGraphSelection({
|
|||
/>
|
||||
<DropdownButton
|
||||
text='Инвертировать'
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Инвертировать', '6') : undefined}
|
||||
icon={<IconGraphInverse size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectInvert}
|
||||
/>
|
||||
|
|
@ -181,25 +197,29 @@ export function ToolbarGraphSelection({
|
|||
<Dropdown isOpen={isGroupOpen} stretchLeft>
|
||||
<DropdownButton
|
||||
text='ядро'
|
||||
title='Выделить ядро'
|
||||
title={!tipHotkeys ? 'Выделить ядро' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить ядро', 'Z') : undefined}
|
||||
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectCore}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='ключевые'
|
||||
title='Выделить ключевые'
|
||||
title={!tipHotkeys ? 'Выделить ключевые' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить ключевые', 'X') : undefined}
|
||||
icon={<IconCrucial size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectCrucial}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='собственные'
|
||||
title='Выделить собственные'
|
||||
title={!tipHotkeys ? 'Выделить собственные' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить собственные', 'C') : undefined}
|
||||
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectOwned}
|
||||
/>
|
||||
<DropdownButton
|
||||
text='наследники'
|
||||
title='Выделить наследников'
|
||||
title={!tipHotkeys ? 'Выделить наследников' : undefined}
|
||||
titleHtml={tipHotkeys ? prepareTooltip('Выделить наследников', 'Y') : undefined}
|
||||
icon={<IconChild size='1.25rem' className='icon-primary' />}
|
||||
onClick={handleSelectInherited}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Controller, useForm } from 'react-hook-form';
|
|||
import { MiniButton } from '@/components/control';
|
||||
import { Checkbox } from '@/components/input';
|
||||
import { ModalForm } from '@/components/modal';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { CstType } from '../backend/types';
|
||||
import { IconCstType } from '../components/icon-cst-type';
|
||||
|
|
@ -35,19 +36,31 @@ export function DlgGraphParams() {
|
|||
<Controller
|
||||
control={control}
|
||||
name='noText'
|
||||
render={({ field }) => <Checkbox {...field} label='Скрыть текст' title='Не отображать термины' />}
|
||||
render={({ field }) => (
|
||||
<Checkbox {...field} label='Скрыть текст' titleHtml={prepareTooltip('Не отображать термины', 'T')} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='foldDerived'
|
||||
render={({ field }) => (
|
||||
<Checkbox {...field} label='Скрыть порожденные' title='Не отображать порожденные понятия' />
|
||||
<Checkbox
|
||||
{...field}
|
||||
label='Скрыть порожденные'
|
||||
titleHtml={prepareTooltip('Не отображать порожденные понятия', 'V')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='noHermits'
|
||||
render={({ field }) => <Checkbox {...field} label='Скрыть несвязанные' title='Неиспользуемые конституенты' />}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
label='Скрыть несвязанные'
|
||||
titleHtml={prepareTooltip('Неиспользуемые конституенты', 'B')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
|
|
@ -72,9 +85,9 @@ export function DlgGraphParams() {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-col items-center gap-1'>
|
||||
<h1 className='mb-1'>Типы конституент</h1>
|
||||
<div>
|
||||
<div className='grid grid-cols-3'>
|
||||
{Object.values(CstType).map(cstType => {
|
||||
const fieldName = cstTypeToFilterKey[cstType];
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -328,7 +328,8 @@ export function sortItemsForInlineSynthesis(receiver: IRSForm, items: readonly I
|
|||
|
||||
/** Remove alias from expression. */
|
||||
export function removeAliasReference(expression: string, alias: string): string {
|
||||
return expression.replaceAll(new RegExp(`\\b${alias}\\b`, 'g'), 'DEL');
|
||||
const result = expression.replaceAll(new RegExp(`\\b${alias}\\b`, 'g'), 'DEL');
|
||||
return result === 'DEL' ? '' : result;
|
||||
}
|
||||
|
||||
/** Add alias to expression. */
|
||||
|
|
|
|||
|
|
@ -148,7 +148,6 @@ export interface IRSForm extends ILibraryItemData {
|
|||
stats: IRSFormStats;
|
||||
graph: Graph;
|
||||
attribution_graph: Graph;
|
||||
full_graph: Graph;
|
||||
cstByAlias: Map<string, IConstituenta>;
|
||||
cstByID: Map<number, IConstituenta>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ import {
|
|||
import clsx from 'clsx';
|
||||
|
||||
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
|
||||
import { useContinuousPan } from '@/components/flow/use-continous-panning';
|
||||
import { useWindowSize } from '@/hooks/use-window-size';
|
||||
import { useFitHeight, useMainHeight } from '@/stores/app-layout';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
import { withPreventDefault } from '@/utils/utils';
|
||||
|
||||
import { ParsingStatus } from '../../../backend/types';
|
||||
import { CstType, ParsingStatus } from '../../../backend/types';
|
||||
import { useCreateAttribution } from '../../../backend/use-create-attribution';
|
||||
import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
|
||||
import { useUpdateConstituenta } from '../../../backend/use-update-constituenta';
|
||||
|
|
@ -33,7 +34,7 @@ import { SelectColoring } from '../../../components/term-graph/select-coloring';
|
|||
import { SelectEdgeType } from '../../../components/term-graph/select-edge-type';
|
||||
import { ViewHidden } from '../../../components/term-graph/view-hidden';
|
||||
import { applyLayout, inferEdgeType, type TGNodeData } from '../../../models/graph-api';
|
||||
import { addAliasReference } from '../../../models/rsform-api';
|
||||
import { addAliasReference, isBasicConcept } from '../../../models/rsform-api';
|
||||
import { InteractionMode, TGEdgeType, useTermGraphStore, useTGConnectionStore } from '../../../stores/term-graph';
|
||||
import { useRSEdit } from '../rsedit-context';
|
||||
|
||||
|
|
@ -55,6 +56,9 @@ export function TGFlow() {
|
|||
const { isSmall } = useWindowSize();
|
||||
const mainHeight = useMainHeight();
|
||||
const { fitView, viewportInitialized } = useReactFlow();
|
||||
const flowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useContinuousPan(flowRef);
|
||||
|
||||
const mode = useTermGraphStore(state => state.mode);
|
||||
const toggleMode = useTermGraphStore(state => state.toggleMode);
|
||||
|
|
@ -62,6 +66,8 @@ export function TGFlow() {
|
|||
const setConnectionStart = useTGConnectionStore(state => state.setStart);
|
||||
const connectionType = useTGConnectionStore(state => state.connectionType);
|
||||
const toggleText = useTermGraphStore(state => state.toggleText);
|
||||
const toggleClustering = useTermGraphStore(state => state.toggleClustering);
|
||||
const toggleHermits = useTermGraphStore(state => state.toggleHermits);
|
||||
|
||||
const { createAttribution } = useCreateAttribution();
|
||||
const { updateConstituenta } = useUpdateConstituenta();
|
||||
|
|
@ -79,7 +85,8 @@ export function TGFlow() {
|
|||
navigateCst,
|
||||
selectedEdges,
|
||||
setSelectedEdges,
|
||||
deselectAll
|
||||
deselectAll,
|
||||
createCst
|
||||
} = useRSEdit();
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||
|
|
@ -90,14 +97,9 @@ export function TGFlow() {
|
|||
const hiddenHeight = useFitHeight(isSmall ? '15rem + 2px' : '13.5rem + 2px', '4rem');
|
||||
|
||||
function onSelectionChange({ nodes, edges }: { nodes: Node[]; edges: Edge[] }) {
|
||||
if (mode === InteractionMode.explore) {
|
||||
const ids = nodes.map(node => Number(node.id));
|
||||
setSelectedCst(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
|
||||
setSelectedEdges([]);
|
||||
} else {
|
||||
setSelectedCst([]);
|
||||
setSelectedEdges(edges.map(edge => edge.id));
|
||||
}
|
||||
const ids = nodes.map(node => Number(node.id));
|
||||
setSelectedCst(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
|
||||
setSelectedEdges(edges.map(edge => edge.id));
|
||||
}
|
||||
useOnSelectionChange({
|
||||
onChange: onSelectionChange
|
||||
|
|
@ -294,14 +296,118 @@ export function TGFlow() {
|
|||
deselectAll();
|
||||
}
|
||||
|
||||
function handleCreateCst() {
|
||||
const definition = selectedCst.map(id => schema.cstByID.get(id)!.alias).join(' ');
|
||||
createCst(selectedCst.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||
}
|
||||
|
||||
function handleSelectCore() {
|
||||
const isCore = (cstID: number) => {
|
||||
const cst = schema.cstByID.get(cstID);
|
||||
return !!cst && isBasicConcept(cst.cst_type);
|
||||
};
|
||||
const core = [...filteredGraph.nodes.keys()].filter(isCore);
|
||||
setSelectedCst([...core, ...filteredGraph.expandInputs(core)]);
|
||||
}
|
||||
|
||||
function handleSelectOwned() {
|
||||
setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => !schema.cstByID.get(cstID)?.is_inherited));
|
||||
}
|
||||
|
||||
function handleSelectInherited() {
|
||||
setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.is_inherited ?? false));
|
||||
}
|
||||
|
||||
function handleSelectCrucial() {
|
||||
setSelectedCst([...filteredGraph.nodes.keys()].filter(cstID => schema.cstByID.get(cstID)?.crucial ?? false));
|
||||
}
|
||||
|
||||
function handleExpandOutputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandOutputs(prev)]);
|
||||
}
|
||||
|
||||
function handleExpandInputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandInputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectMaximize() {
|
||||
setSelectedCst(prev => filteredGraph.maximizePart(prev));
|
||||
}
|
||||
|
||||
function handleSelectInvert() {
|
||||
setSelectedCst(prev => [...filteredGraph.nodes.keys()].filter(item => !prev.includes(item)));
|
||||
}
|
||||
|
||||
function handleSelectAllInputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandAllInputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectAllOutputs() {
|
||||
setSelectedCst(prev => [...prev, ...filteredGraph.expandAllOutputs(prev)]);
|
||||
}
|
||||
|
||||
function handleSelectionHotkey(eventCode: string): boolean {
|
||||
if (eventCode === 'Escape') {
|
||||
setFocus(null);
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit1') {
|
||||
handleExpandInputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit2') {
|
||||
handleExpandOutputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit3') {
|
||||
handleSelectAllInputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit4') {
|
||||
handleSelectAllOutputs();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit5') {
|
||||
handleSelectMaximize();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'Digit6') {
|
||||
handleSelectInvert();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyZ') {
|
||||
handleSelectCore();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyX') {
|
||||
handleSelectCrucial();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyC') {
|
||||
handleSelectOwned();
|
||||
return true;
|
||||
}
|
||||
if (eventCode === 'KeyY') {
|
||||
handleSelectInherited();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Escape') {
|
||||
withPreventDefault(() => setFocus(null))(event);
|
||||
if (event.shiftKey || event.ctrlKey || event.metaKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
if (handleSelectionHotkey(event.code)) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 'KeyG') {
|
||||
withPreventDefault(() => fitView(flowOptions.fitViewOptions))(event);
|
||||
return;
|
||||
|
|
@ -310,8 +416,16 @@ export function TGFlow() {
|
|||
withPreventDefault(toggleText)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyV') {
|
||||
withPreventDefault(toggleClustering)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'KeyB') {
|
||||
withPreventDefault(toggleHermits)(event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContentEditable && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
||||
if (isContentEditable) {
|
||||
if (event.code === 'KeyQ') {
|
||||
withPreventDefault(handleToggleMode)(event);
|
||||
return;
|
||||
|
|
@ -320,7 +434,11 @@ export function TGFlow() {
|
|||
withPreventDefault(toggleEdgeType)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Delete') {
|
||||
if (event.code === 'KeyR') {
|
||||
withPreventDefault(handleCreateCst)(event);
|
||||
return;
|
||||
}
|
||||
if (event.code === 'Delete' || event.code === 'Backquote') {
|
||||
withPreventDefault(handleDeleteSelected)(event);
|
||||
return;
|
||||
}
|
||||
|
|
@ -329,6 +447,7 @@ export function TGFlow() {
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={flowRef}
|
||||
className={clsx('relative', mode === InteractionMode.explore ? 'mode-explore' : 'mode-edit')}
|
||||
tabIndex={-1}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
|
|||
onClick={toggleText}
|
||||
/>
|
||||
<MiniButton
|
||||
title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
|
||||
titleHtml={prepareTooltip(!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные', 'V')}
|
||||
icon={
|
||||
!filter.foldDerived ? (
|
||||
<IconClustering size='1.25rem' className='icon-green' />
|
||||
|
|
@ -174,6 +174,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
|
|||
{focusCst ? <ToolbarFocusedCst resetFocus={() => setFocus(null)} /> : null}
|
||||
{!focusCst && mode === InteractionMode.explore ? (
|
||||
<ToolbarGraphSelection
|
||||
tipHotkeys
|
||||
graph={filteredGraph}
|
||||
isCore={cstID => {
|
||||
const cst = schema.cstByID.get(cstID);
|
||||
|
|
@ -203,7 +204,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
|
|||
) : null}
|
||||
{isContentEditable ? (
|
||||
<MiniButton
|
||||
title='Новая конституента'
|
||||
titleHtml={prepareTooltip('Новая конституента', 'R')}
|
||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||
onClick={handleCreateCst}
|
||||
disabled={isProcessing}
|
||||
|
|
@ -211,7 +212,7 @@ export function ToolbarTermGraph({ className, onDeleteSelected }: ToolbarTermGra
|
|||
) : null}
|
||||
{isContentEditable ? (
|
||||
<MiniButton
|
||||
title='Удалить выбранные'
|
||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete, `')}
|
||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||
onClick={onDeleteSelected}
|
||||
disabled={!canDeleteSelected || isProcessing}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import { useRSFormSuspense } from '../../backend/use-rsform';
|
|||
import { useUpdateConstituenta } from '../../backend/use-update-constituenta';
|
||||
import { type IConstituenta } from '../../models/rsform';
|
||||
import { generateAlias, removeAliasReference } from '../../models/rsform-api';
|
||||
import { InteractionMode, useTermGraphStore } from '../../stores/term-graph';
|
||||
|
||||
import { RSEditContext, RSTabID } from './rsedit-context';
|
||||
|
||||
|
|
@ -58,13 +57,12 @@ export const RSEditState = ({
|
|||
const isContentEditable = isMutable && !isArchive;
|
||||
const isAttachedToOSS = schema.oss.length > 0;
|
||||
const isEditor = !!user.id && schema.editors.includes(user.id);
|
||||
const mode = useTermGraphStore(state => state.mode);
|
||||
|
||||
const [selectedCst, setSelectedCst] = useState<number[]>([]);
|
||||
const [selectedEdges, setSelectedEdges] = useState<string[]>([]);
|
||||
const canDeleteSelected =
|
||||
(selectedCst.length > 0 && selectedCst.every(id => !schema.cstByID.get(id)?.is_inherited)) ||
|
||||
(selectedEdges.length === 1 && mode === InteractionMode.edit);
|
||||
(selectedCst.length === 0 && selectedEdges.length === 1);
|
||||
const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
|
||||
|
||||
const activeCst = selectedCst.length === 0 ? null : schema.cstByID.get(selectedCst[selectedCst.length - 1])!;
|
||||
|
|
@ -268,9 +266,9 @@ export const RSEditState = ({
|
|||
if (!canDeleteSelected) {
|
||||
return;
|
||||
}
|
||||
if (mode === InteractionMode.explore || selectedEdges.length === 0) {
|
||||
if (selectedCst.length > 0) {
|
||||
deleteSelectedCst();
|
||||
} else {
|
||||
} else if (selectedEdges.length === 1) {
|
||||
deleteSelectedEdge();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ interface TermGraphStore {
|
|||
toggleText: () => void;
|
||||
toggleClustering: () => void;
|
||||
toggleGraphType: () => void;
|
||||
toggleHermits: () => void;
|
||||
|
||||
foldHidden: boolean;
|
||||
toggleFoldHidden: () => void;
|
||||
|
|
@ -125,6 +126,7 @@ export const useTermGraphStore = create<TermGraphStore>()(
|
|||
toggleFocusOutputs: () =>
|
||||
set(state => ({ filter: { ...state.filter, focusShowOutputs: !state.filter.focusShowOutputs } })),
|
||||
toggleText: () => set(state => ({ filter: { ...state.filter, noText: !state.filter.noText } })),
|
||||
toggleHermits: () => set(state => ({ filter: { ...state.filter, noHermits: !state.filter.noHermits } })),
|
||||
toggleClustering: () => set(state => ({ filter: { ...state.filter, foldDerived: !state.filter.foldDerived } })),
|
||||
toggleGraphType: () =>
|
||||
set(state => ({
|
||||
|
|
|
|||
|
|
@ -84,11 +84,26 @@
|
|||
cursor: inherit;
|
||||
|
||||
&.selected {
|
||||
filter: drop-shadow(0 0 4px var(--color-primary));
|
||||
filter: drop-shadow(0 0 4px var(--color-primary)) drop-shadow(0 0 6px var(--color-primary));
|
||||
|
||||
.dark & {
|
||||
filter: drop-shadow(0 0 4px var(--color-accent-orange-foreground))
|
||||
drop-shadow(0 0 6px var(--color-accent-orange-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
.mode-edit & {
|
||||
cursor: pointer;
|
||||
.mode-space & {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rf-edge-events {
|
||||
pointer-events: stroke;
|
||||
stroke-width: 6;
|
||||
stroke: transparent;
|
||||
|
||||
.mode-space & {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user