diff --git a/src/composables/states/appCanvas/align/alignBoxSelectedState.ts b/src/composables/states/appCanvas/align/alignBoxSelectedState.ts index 1ee00ff0..881a4a73 100644 --- a/src/composables/states/appCanvas/align/alignBoxSelectedState.ts +++ b/src/composables/states/appCanvas/align/alignBoxSelectedState.ts @@ -50,7 +50,7 @@ export function newAlignBoxSelectedState(): AppCanvasState { targetId = ctx.getLastSelectedShapeId()!; ctx.showFloatMenu(); - ctx.setCommandExams(getCommonCommandExams()); + ctx.setCommandExams(getCommonCommandExams(ctx)); initHandler(ctx); }, onEnd: (ctx) => { diff --git a/src/composables/states/appCanvas/board/boardEntitySelectedState.ts b/src/composables/states/appCanvas/board/boardEntitySelectedState.ts index 6ebc5209..8ded296d 100644 --- a/src/composables/states/appCanvas/board/boardEntitySelectedState.ts +++ b/src/composables/states/appCanvas/board/boardEntitySelectedState.ts @@ -73,7 +73,7 @@ export function newBoardEntitySelectedState(): AppCanvasState { }); ctx.showFloatMenu(); - ctx.setCommandExams(getCommonCommandExams()); + ctx.setCommandExams(getCommonCommandExams(ctx)); initHandler(ctx); }, onEnd: (ctx) => { diff --git a/src/composables/states/appCanvas/commandExams.ts b/src/composables/states/appCanvas/commandExams.ts index 44c2c8d9..0dbffb7e 100644 --- a/src/composables/states/appCanvas/commandExams.ts +++ b/src/composables/states/appCanvas/commandExams.ts @@ -28,4 +28,7 @@ export const COMMAND_EXAM_SRC = { GROUP: { command: `${getCtrlOrMetaStr()} + g`, title: "Group" }, UNGROUP: { command: `${getCtrlOrMetaStr()} + G`, title: "Ungroup" }, + + SELECT_PARENT: { command: "p", title: "Select parent" }, + SELECT_CHILD: { command: "c", title: "Select child" }, } satisfies { [key: string]: CommandExam }; diff --git a/src/composables/states/appCanvas/commons.ts b/src/composables/states/appCanvas/commons.ts index 419056d1..ceda4de6 100644 --- a/src/composables/states/appCanvas/commons.ts +++ b/src/composables/states/appCanvas/commons.ts @@ -38,6 +38,7 @@ import { newSingleSelectedByPointerOnState } from "./singleSelectedByPointerOnSt import { newMovingHubState } from "./movingHubState"; import { getPatchByLayouts } from "../../shapeLayoutHandler"; import { ShapeSelectionScope } from "../../../shapes/core"; +import { CommandExam } from "../types"; type AcceptableEvent = "Break" | "DroppingNewShape" | "LineReady" | "TextReady"; @@ -90,6 +91,24 @@ export function handleCommonShortcut( } return newSelectionHubState; } + case "p": { + const shapeComposite = ctx.getShapeComposite(); + const current = shapeComposite.shapeMap[ctx.getLastSelectedShapeId() ?? ""]; + if (current?.parentId && shapeComposite.shapeMap[current.parentId]) { + ctx.selectShape(current.parentId); + return newSelectionHubState; + } + return; + } + case "c": { + const shapeComposite = ctx.getShapeComposite(); + const currentNode = shapeComposite.mergedShapeTreeMap[ctx.getLastSelectedShapeId() ?? ""]; + if (currentNode && currentNode.children.length > 0 && shapeComposite.shapeMap[currentNode.children[0].id]) { + ctx.selectShape(currentNode.children[0].id); + return newSelectionHubState; + } + return; + } case "g": if (event.data.ctrl) { event.data.prevent?.(); @@ -168,8 +187,30 @@ const COMMON_COMMAND_EXAMS = [ COMMAND_EXAM_SRC.PAN_CANVAS, COMMAND_EXAM_SRC.RESET_VIEWPORT, ]; -export function getCommonCommandExams() { - return COMMON_COMMAND_EXAMS; +export function getCommonCommandExams(ctx: AppCanvasStateContext): CommandExam[] { + const shapeComposite = ctx.getShapeComposite(); + const shapeMap = shapeComposite.shapeMap; + const current = shapeMap[ctx.getLastSelectedShapeId() ?? ""]; + if (!current) return COMMON_COMMAND_EXAMS; + + const extra: CommandExam[] = []; + if (shapeComposite.shapeMap[current.parentId ?? ""]) { + extra.push(COMMAND_EXAM_SRC.SELECT_PARENT); + } + const currentNode = shapeComposite.mergedShapeTreeMap[current.id]; + if (currentNode.children.length > 0 && shapeMap[currentNode.children[0].id]) { + extra.push(COMMAND_EXAM_SRC.SELECT_CHILD); + } + + const selectedIds = Object.keys(ctx.getSelectedShapeIdMap()); + if (canGroupShapes(shapeComposite, selectedIds)) { + extra.push(COMMAND_EXAM_SRC.GROUP); + } + if (selectedIds.some((id) => shapeMap[id] && isGroupShape(shapeMap[id]))) { + extra.push(COMMAND_EXAM_SRC.UNGROUP); + } + + return extra.length > 0 ? [...extra, ...COMMON_COMMAND_EXAMS] : COMMON_COMMAND_EXAMS; } export function handleCommonTextStyle( diff --git a/src/composables/states/appCanvas/defaultState.ts b/src/composables/states/appCanvas/defaultState.ts index 253d0326..23dacdac 100644 --- a/src/composables/states/appCanvas/defaultState.ts +++ b/src/composables/states/appCanvas/defaultState.ts @@ -20,7 +20,7 @@ export function newDefaultState(): AppCanvasState { const state: AppCanvasState = { getLabel: () => "Default", onStart(ctx) { - ctx.setCommandExams(getCommonCommandExams()); + ctx.setCommandExams(getCommonCommandExams(ctx)); }, onEnd(ctx) { ctx.setCommandExams(); diff --git a/src/composables/states/appCanvas/lines/lineLabelSelectedState.ts b/src/composables/states/appCanvas/lines/lineLabelSelectedState.ts index 7a82f7fc..7516e51f 100644 --- a/src/composables/states/appCanvas/lines/lineLabelSelectedState.ts +++ b/src/composables/states/appCanvas/lines/lineLabelSelectedState.ts @@ -36,7 +36,7 @@ export function newLineLabelSelectedState(option?: Option): AppCanvasState { return { getLabel: () => "LineLabelSelected", onStart: (ctx) => { - ctx.setCommandExams(getCommonCommandExams()); + ctx.setCommandExams(getCommonCommandExams(ctx)); const shapeComposite = ctx.getShapeComposite(); const shapeMap = shapeComposite.shapeMap; diff --git a/src/composables/states/appCanvas/lines/lineSelectedState.ts b/src/composables/states/appCanvas/lines/lineSelectedState.ts index 460a5c7f..fa5bcbba 100644 --- a/src/composables/states/appCanvas/lines/lineSelectedState.ts +++ b/src/composables/states/appCanvas/lines/lineSelectedState.ts @@ -35,7 +35,7 @@ export function newLineSelectedState(): AppCanvasState { ctx.showFloatMenu(); lineShape = ctx.getShapeComposite().shapeMap[ctx.getLastSelectedShapeId() ?? ""] as LineShape; lineBounding = newLineBounding({ lineShape, scale: ctx.getScale(), styleScheme: ctx.getStyleScheme() }); - ctx.setCommandExams([COMMAND_EXAM_SRC.DELETE_INER_VERTX, ...getCommonCommandExams()]); + ctx.setCommandExams([COMMAND_EXAM_SRC.DELETE_INER_VERTX, ...getCommonCommandExams(ctx)]); }, onEnd: (ctx) => { ctx.hideFloatMenu(); diff --git a/src/composables/states/appCanvas/multipleSelectedState.ts b/src/composables/states/appCanvas/multipleSelectedState.ts index 43f529ac..1baa0e77 100644 --- a/src/composables/states/appCanvas/multipleSelectedState.ts +++ b/src/composables/states/appCanvas/multipleSelectedState.ts @@ -21,9 +21,7 @@ import { newRectangleSelectingState } from "./ractangleSelectingState"; import { newDuplicatingShapesState } from "./duplicatingShapesState"; import { newSelectionHubState } from "./selectionHubState"; import { CONTEXT_MENU_ITEM_SRC, handleContextItemEvent } from "./contextMenuItems"; -import { COMMAND_EXAM_SRC } from "./commandExams"; -import { canGroupShapes, findBetterShapeAt, getRotatedTargetBounds } from "../../shapeComposite"; -import { isGroupShape } from "../../../shapes/group"; +import { findBetterShapeAt, getRotatedTargetBounds } from "../../shapeComposite"; import { newMovingHubState } from "./movingHubState"; import { ShapeSelectionScope, isSameShapeSelectionScope } from "../../../shapes/core"; @@ -73,13 +71,7 @@ export function newMultipleSelectedState(option?: Option): AppCanvasState { } ctx.showFloatMenu(); - if (selectedIds.some((id) => isGroupShape(shapeMap[id]))) { - ctx.setCommandExams([COMMAND_EXAM_SRC.GROUP, COMMAND_EXAM_SRC.UNGROUP, ...getCommonCommandExams()]); - } else if (canGroupShapes(shapeComposite, selectedIds)) { - ctx.setCommandExams([COMMAND_EXAM_SRC.GROUP, ...getCommonCommandExams()]); - } else { - ctx.setCommandExams(getCommonCommandExams()); - } + ctx.setCommandExams(getCommonCommandExams(ctx)); if (option?.boundingBox) { // Recalculate the bounding because shapes aren't always transformed along with the bounding box. diff --git a/src/composables/states/appCanvas/singleSelectedState.ts b/src/composables/states/appCanvas/singleSelectedState.ts index 0402874f..2e2ed68f 100644 --- a/src/composables/states/appCanvas/singleSelectedState.ts +++ b/src/composables/states/appCanvas/singleSelectedState.ts @@ -21,7 +21,6 @@ import { getOuterRectangle } from "okageo"; import { newSelectionHubState } from "./selectionHubState"; import { CONTEXT_MENU_ITEM_SRC, handleContextItemEvent } from "./contextMenuItems"; import { isGroupShape } from "../../../shapes/group"; -import { COMMAND_EXAM_SRC } from "./commandExams"; import { findBetterShapeAt } from "../../shapeComposite"; import { ShapeSelectionScope } from "../../../shapes/core"; @@ -45,11 +44,7 @@ export function newSingleSelectedState(): AppCanvasState { selectionScope = shapeComposite.getSelectionScope(shape); isGroupShapeSelected = isGroupShape(shape); - if (isGroupShapeSelected) { - ctx.setCommandExams([COMMAND_EXAM_SRC.UNGROUP, ...getCommonCommandExams()]); - } else { - ctx.setCommandExams(getCommonCommandExams()); - } + ctx.setCommandExams(getCommonCommandExams(ctx)); boundingBox = newBoundingBox({ path: shapeComposite.getLocalRectPolygon(shape), diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 0c1dede5..8001e8b0 100644 --- a/src/utils/tree.ts +++ b/src/utils/tree.ts @@ -40,13 +40,13 @@ function getChildNodes(parentMap: { [id: string]: T[] }, /** * Depth first ordered */ -export function walkTree(treeNodes: TreeNode[], fn: (node: TreeNode) => void) { - treeNodes.forEach((n) => walkTreeStep(n, fn)); +export function walkTree(treeNodes: TreeNode[], fn: (node: TreeNode, i: number) => void) { + treeNodes.forEach((n, i) => walkTreeStep(n, fn, i)); } -function walkTreeStep(node: TreeNode, fn: (node: TreeNode) => void) { - fn(node); - node.children.forEach((c) => walkTreeStep(c, fn)); +function walkTreeStep(node: TreeNode, fn: (node: TreeNode, i: number) => void, i: number) { + fn(node, i); + node.children.forEach((c, j) => walkTreeStep(c, fn, j)); } /**