Add support for locking shapes (#1447)
Add support for locking shapes. How it works right now: - You can lock / unlock shapes from the context menu. - You can also lock shapes with `⇧⌘L` keyboard shortcut. - You cannot select locked shapes: clicking on the shape, double click to edit, select all, brush select,... should not work. - You cannot change props of locked shapes. - You cannot delete locked shapes. - If a shape is grouped or within the frame the same rules apply. - If you delete a group, that contains locked shape it will also delete those shapes. This seems to be what other apps use as well. Solves #1445 ### Change Type - [x] `minor` — New Feature ### Test Plan 1. Insert a shape 2. Right click on it and lock it. 3. Test that you cannot select it, change its properties, delete it. 4. Do the same with locked groups. 5. Do the same with locked frames. - [x] Unit Tests - [ ] Webdriver tests ### Release Notes - Add support for locking shapes. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
a3c39cde4b
commit
d738c28c19
19 changed files with 328 additions and 68 deletions
|
@ -79,6 +79,7 @@
|
||||||
"action.toggle-focus-mode": "Toggle focus mode",
|
"action.toggle-focus-mode": "Toggle focus mode",
|
||||||
"action.toggle-grid.menu": "Show grid",
|
"action.toggle-grid.menu": "Show grid",
|
||||||
"action.toggle-grid": "Toggle grid",
|
"action.toggle-grid": "Toggle grid",
|
||||||
|
"action.toggle-lock": "Lock / Unlock",
|
||||||
"action.toggle-snap-mode.menu": "Always snap",
|
"action.toggle-snap-mode.menu": "Always snap",
|
||||||
"action.toggle-snap-mode": "Toggle always snap",
|
"action.toggle-snap-mode": "Toggle always snap",
|
||||||
"action.toggle-tool-lock.menu": "Tool lock",
|
"action.toggle-tool-lock.menu": "Tool lock",
|
||||||
|
|
|
@ -360,14 +360,13 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
new (...args: any): TLShapeUtil<T>;
|
new (...args: any): TLShapeUtil<T>;
|
||||||
type: string;
|
type: string;
|
||||||
}): shape is T;
|
}): shape is T;
|
||||||
|
isShapeOrAncestorLocked(shape?: TLShape): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get isSnapMode(): boolean;
|
get isSnapMode(): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get isToolLocked(): boolean;
|
get isToolLocked(): boolean;
|
||||||
isWithinSelection(id: TLShapeId): boolean;
|
isWithinSelection(id: TLShapeId): boolean;
|
||||||
get locale(): string;
|
get locale(): string;
|
||||||
// (undocumented)
|
|
||||||
lockShapes(_ids?: TLShapeId[]): this;
|
|
||||||
mark(reason?: string, onUndo?: boolean, onRedo?: boolean): string;
|
mark(reason?: string, onUndo?: boolean, onRedo?: boolean): string;
|
||||||
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
|
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
|
||||||
nudgeShapes(ids: TLShapeId[], direction: Vec2dModel, major?: boolean, ephemeral?: boolean): this;
|
nudgeShapes(ids: TLShapeId[], direction: Vec2dModel, major?: boolean, ephemeral?: boolean): this;
|
||||||
|
@ -505,6 +504,8 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
|
stretchShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
|
||||||
static styles: TLStyleCollections;
|
static styles: TLStyleCollections;
|
||||||
textMeasure: TextManager;
|
textMeasure: TextManager;
|
||||||
|
// (undocumented)
|
||||||
|
toggleLock(ids?: TLShapeId[]): this;
|
||||||
undo(): HistoryManager<this>;
|
undo(): HistoryManager<this>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
ungroupShapes(ids?: TLShapeId[]): this;
|
ungroupShapes(ids?: TLShapeId[]): this;
|
||||||
|
@ -2033,11 +2034,11 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
canBind: () => boolean;
|
canBind: () => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
canDropShapes: (_shape: TLFrameShape, _shapes: TLShape[]) => boolean;
|
canDropShapes: (shape: TLFrameShape, _shapes: TLShape[]) => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
canEdit: () => boolean;
|
canEdit: () => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
canReceiveNewChildrenOfType: (_type: TLShape['type']) => boolean;
|
canReceiveNewChildrenOfType: (shape: TLShape, _type: TLShape['type']) => boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
defaultProps(): TLFrameShape['props'];
|
defaultProps(): TLFrameShape['props'];
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -2493,7 +2494,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
||||||
canCrop: TLShapeUtilFlag<T>;
|
canCrop: TLShapeUtilFlag<T>;
|
||||||
canDropShapes(shape: T, shapes: TLShape[]): boolean;
|
canDropShapes(shape: T, shapes: TLShape[]): boolean;
|
||||||
canEdit: TLShapeUtilFlag<T>;
|
canEdit: TLShapeUtilFlag<T>;
|
||||||
canReceiveNewChildrenOfType(type: TLShape['type']): boolean;
|
canReceiveNewChildrenOfType(shape: T, type: TLShape['type']): boolean;
|
||||||
canResize: TLShapeUtilFlag<T>;
|
canResize: TLShapeUtilFlag<T>;
|
||||||
canScroll: TLShapeUtilFlag<T>;
|
canScroll: TLShapeUtilFlag<T>;
|
||||||
canUnmount: TLShapeUtilFlag<T>;
|
canUnmount: TLShapeUtilFlag<T>;
|
||||||
|
|
|
@ -2221,6 +2221,18 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
return this.viewportPageBounds.includes(pageBounds)
|
return this.viewportPageBounds.includes(pageBounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a shape or its parent is locked.
|
||||||
|
*
|
||||||
|
* @param id - The id of the shape to check.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
isShapeOrAncestorLocked(shape?: TLShape): boolean {
|
||||||
|
if (shape === undefined) return false
|
||||||
|
if (shape.isLocked) return true
|
||||||
|
return this.isShapeOrAncestorLocked(this.getParentShape(shape))
|
||||||
|
}
|
||||||
|
|
||||||
private computeUnorderedRenderingShapes(
|
private computeUnorderedRenderingShapes(
|
||||||
ids: TLParentId[],
|
ids: TLParentId[],
|
||||||
{
|
{
|
||||||
|
@ -2940,7 +2952,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
for (let i = shapes.length - 1; i >= 0; i--) {
|
for (let i = shapes.length - 1; i >= 0; i--) {
|
||||||
const shape = shapes[i]
|
const shape = shapes[i]
|
||||||
const util = this.getShapeUtil(shape)
|
const util = this.getShapeUtil(shape)
|
||||||
if (!util.canReceiveNewChildrenOfType(shapeType)) continue
|
if (!util.canReceiveNewChildrenOfType(shape, shapeType)) continue
|
||||||
const maskedPageBounds = this.getMaskedPageBoundsById(shape.id)
|
const maskedPageBounds = this.getMaskedPageBoundsById(shape.id)
|
||||||
if (
|
if (
|
||||||
maskedPageBounds &&
|
maskedPageBounds &&
|
||||||
|
@ -4897,17 +4909,21 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
updateShapes(partials: (TLShapePartial | null | undefined)[], squashing = false) {
|
updateShapes(partials: (TLShapePartial | null | undefined)[], squashing = false) {
|
||||||
|
let compactedPartials = compact(partials)
|
||||||
if (this.animatingShapes.size > 0) {
|
if (this.animatingShapes.size > 0) {
|
||||||
let partial: TLShapePartial | null | undefined
|
compactedPartials.forEach((p) => this.animatingShapes.delete(p.id))
|
||||||
for (let i = 0; i < partials.length; i++) {
|
|
||||||
partial = partials[i]
|
|
||||||
if (partial) {
|
|
||||||
this.animatingShapes.delete(partial.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._updateShapes(partials, squashing)
|
compactedPartials = compactedPartials.filter((p) => {
|
||||||
|
const shape = this.getShapeById(p.id)
|
||||||
|
if (!shape) return false
|
||||||
|
|
||||||
|
// Only allow changes to unlocked shapes or changes to the isLocked property (otherwise we cannot unlock a shape)
|
||||||
|
if (this.isShapeOrAncestorLocked(shape) && !Object.hasOwn(p, 'isLocked')) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
this._updateShapes(compactedPartials, squashing)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5001,6 +5017,11 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
private _getUnlockedShapeIds(ids: TLShapeId[]): TLShapeId[] {
|
||||||
|
return ids.filter((id) => !this.getShapeById(id)?.isLocked)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete shapes.
|
* Delete shapes.
|
||||||
*
|
*
|
||||||
|
@ -5015,7 +5036,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
deleteShapes(ids: TLShapeId[] = this.selectedIds) {
|
deleteShapes(ids: TLShapeId[] = this.selectedIds) {
|
||||||
this._deleteShapes(ids)
|
this._deleteShapes(this._getUnlockedShapeIds(ids))
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6003,9 +6024,34 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
lockShapes(_ids: TLShapeId[] = this.pageState.selectedIds): this {
|
toggleLock(ids: TLShapeId[] = this.selectedIds): this {
|
||||||
if (this.isReadOnly) return this
|
if (this.isReadOnly || ids.length === 0) return this
|
||||||
// todo
|
|
||||||
|
let allLocked = true,
|
||||||
|
allUnlocked = true
|
||||||
|
const shapes: TLShape[] = []
|
||||||
|
for (const id of ids) {
|
||||||
|
const shape = this.getShapeById(id)
|
||||||
|
if (shape) {
|
||||||
|
shapes.push(shape)
|
||||||
|
if (shape.isLocked) {
|
||||||
|
allUnlocked = false
|
||||||
|
} else {
|
||||||
|
allLocked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allUnlocked) {
|
||||||
|
this.updateShapes(shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true })))
|
||||||
|
this.setSelectedIds([])
|
||||||
|
} else if (allLocked) {
|
||||||
|
this.updateShapes(
|
||||||
|
shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: false }))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.updateShapes(shapes.map((shape) => ({ id: shape.id, type: shape.type, isLocked: true })))
|
||||||
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7241,7 +7287,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
const ids = this.getSortedChildIds(this.currentPageId)
|
const ids = this.getSortedChildIds(this.currentPageId)
|
||||||
// page might have no shapes
|
// page might have no shapes
|
||||||
if (ids.length <= 0) return this
|
if (ids.length <= 0) return this
|
||||||
this.setSelectedIds(ids)
|
this.setSelectedIds(this._getUnlockedShapeIds(ids))
|
||||||
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
@ -8915,7 +8961,7 @@ export class App extends EventEmitter<TLEventMap> {
|
||||||
|
|
||||||
if (ids.length <= 1) return this
|
if (ids.length <= 1) return this
|
||||||
|
|
||||||
const shapes = compact(ids.map((id) => this.getShapeById(id)))
|
const shapes = compact(this._getUnlockedShapeIds(ids).map((id) => this.getShapeById(id)))
|
||||||
const sortedShapeIds = shapes.sort(sortByIndex).map((s) => s.id)
|
const sortedShapeIds = shapes.sort(sortByIndex).map((s) => s.id)
|
||||||
const pageBounds = Box2d.Common(compact(shapes.map((id) => this.getPageBounds(id))))
|
const pageBounds = Box2d.Common(compact(shapes.map((id) => this.getPageBounds(id))))
|
||||||
|
|
||||||
|
|
|
@ -148,16 +148,16 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
providesBackgroundForChildren(): boolean {
|
override canReceiveNewChildrenOfType = (shape: TLShape, _type: TLShape['type']) => {
|
||||||
|
return !shape.isLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
override providesBackgroundForChildren(): boolean {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override canReceiveNewChildrenOfType = (_type: TLShape['type']) => {
|
override canDropShapes = (shape: TLFrameShape, _shapes: TLShape[]): boolean => {
|
||||||
return true
|
return !shape.isLocked
|
||||||
}
|
|
||||||
|
|
||||||
override canDropShapes = (_shape: TLFrameShape, _shapes: TLShape[]): boolean => {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]): { shouldHint: boolean } => {
|
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]): { shouldHint: boolean } => {
|
||||||
|
|
|
@ -316,7 +316,7 @@ export abstract class TLShapeUtil<T extends TLUnknownShape = TLUnknownShape> {
|
||||||
* @param type - The shape type.
|
* @param type - The shape type.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
canReceiveNewChildrenOfType(type: TLShape['type']) {
|
canReceiveNewChildrenOfType(shape: T, type: TLShape['type']) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,8 +21,9 @@ export class Erasing extends StateNode {
|
||||||
this.app.shapesArray
|
this.app.shapesArray
|
||||||
.filter(
|
.filter(
|
||||||
(shape) =>
|
(shape) =>
|
||||||
(shape.type === 'frame' || shape.type === 'group') &&
|
this.app.isShapeOrAncestorLocked(shape) ||
|
||||||
this.app.isPointInShape(originPagePoint, shape)
|
((shape.type === 'group' || shape.type === 'frame') &&
|
||||||
|
this.app.isPointInShape(originPagePoint, shape))
|
||||||
)
|
)
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
)
|
)
|
||||||
|
@ -94,7 +95,6 @@ export class Erasing extends StateNode {
|
||||||
const erasing = new Set<TLShapeId>(erasingIdsSet)
|
const erasing = new Set<TLShapeId>(erasingIdsSet)
|
||||||
|
|
||||||
for (const shape of shapesArray) {
|
for (const shape of shapesArray) {
|
||||||
// Skip groups
|
|
||||||
if (shape.type === 'group') continue
|
if (shape.type === 'group') continue
|
||||||
|
|
||||||
// Avoid testing masked shapes, unless the pointer is inside the mask
|
// Avoid testing masked shapes, unless the pointer is inside the mask
|
||||||
|
|
|
@ -24,6 +24,7 @@ export class Brushing extends StateNode {
|
||||||
|
|
||||||
brush = new Box2d()
|
brush = new Box2d()
|
||||||
initialSelectedIds: TLShapeId[] = []
|
initialSelectedIds: TLShapeId[] = []
|
||||||
|
excludedShapeIds = new Set<TLShapeId>()
|
||||||
|
|
||||||
// The shape that the brush started on
|
// The shape that the brush started on
|
||||||
initialStartShape: TLShape | null = null
|
initialStartShape: TLShape | null = null
|
||||||
|
@ -36,6 +37,12 @@ export class Brushing extends StateNode {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.excludedShapeIds = new Set(
|
||||||
|
this.app.shapesArray
|
||||||
|
.filter((shape) => shape.type === 'group' || this.app.isShapeOrAncestorLocked(shape))
|
||||||
|
.map((shape) => shape.id)
|
||||||
|
)
|
||||||
|
|
||||||
this.info = info
|
this.info = info
|
||||||
this.initialSelectedIds = this.app.selectedIds.slice()
|
this.initialSelectedIds = this.app.selectedIds.slice()
|
||||||
this.initialStartShape = this.app.getShapesAtPoint(currentPagePoint)[0]
|
this.initialStartShape = this.app.getShapesAtPoint(currentPagePoint)[0]
|
||||||
|
@ -104,12 +111,11 @@ export class Brushing extends StateNode {
|
||||||
// We'll be testing the corners of the brush against the shapes
|
// We'll be testing the corners of the brush against the shapes
|
||||||
const { corners } = this.brush
|
const { corners } = this.brush
|
||||||
|
|
||||||
|
const { excludedShapeIds } = this
|
||||||
|
|
||||||
testAllShapes: for (let i = 0, n = shapesArray.length; i < n; i++) {
|
testAllShapes: for (let i = 0, n = shapesArray.length; i < n; i++) {
|
||||||
shape = shapesArray[i]
|
shape = shapesArray[i]
|
||||||
|
if (excludedShapeIds.has(shape.id)) continue testAllShapes
|
||||||
// don't select groups directly, only via their children
|
|
||||||
if (shape.type === 'group') continue testAllShapes
|
|
||||||
|
|
||||||
if (results.has(shape.id)) continue testAllShapes
|
if (results.has(shape.id)) continue testAllShapes
|
||||||
|
|
||||||
pageBounds = this.app.getPageBounds(shape)
|
pageBounds = this.app.getPageBounds(shape)
|
||||||
|
|
|
@ -66,7 +66,11 @@ export class EditingShape extends StateNode {
|
||||||
|
|
||||||
// If the user has clicked onto a different shape of the same type
|
// If the user has clicked onto a different shape of the same type
|
||||||
// which is available to edit, select it and begin editing it.
|
// which is available to edit, select it and begin editing it.
|
||||||
if (shape.type === editingShape.type && util.canEdit?.(shape)) {
|
if (
|
||||||
|
shape.type === editingShape.type &&
|
||||||
|
util.canEdit?.(shape) &&
|
||||||
|
!this.app.isShapeOrAncestorLocked(shape)
|
||||||
|
) {
|
||||||
this.app.setEditingId(shape.id)
|
this.app.setEditingId(shape.id)
|
||||||
this.app.setHoveredId(shape.id)
|
this.app.setHoveredId(shape.id)
|
||||||
this.app.setSelectedIds([shape.id])
|
this.app.setSelectedIds([shape.id])
|
||||||
|
|
|
@ -64,13 +64,13 @@ export class Idle extends StateNode {
|
||||||
this.parent.transition('brushing', info)
|
this.parent.transition('brushing', info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (info.target) {
|
switch (info.target) {
|
||||||
case 'canvas': {
|
case 'canvas': {
|
||||||
this.parent.transition('pointing_canvas', info)
|
this.parent.transition('pointing_canvas', info)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'shape': {
|
case 'shape': {
|
||||||
|
if (this.app.isShapeOrAncestorLocked(info.shape)) break
|
||||||
this.parent.transition('pointing_shape', info)
|
this.parent.transition('pointing_shape', info)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -157,12 +157,15 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// For corners OR edges
|
// For corners OR edges
|
||||||
if (util.canCrop(onlySelectedShape)) {
|
if (
|
||||||
|
util.canCrop(onlySelectedShape) &&
|
||||||
|
!this.app.isShapeOrAncestorLocked(onlySelectedShape)
|
||||||
|
) {
|
||||||
this.parent.transition('crop', info)
|
this.parent.transition('crop', info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (util.canEdit(onlySelectedShape)) {
|
if (this.shouldStartEditingShape(onlySelectedShape)) {
|
||||||
this.startEditingShape(onlySelectedShape, info)
|
this.startEditingShape(onlySelectedShape, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,7 +184,7 @@ export class Idle extends StateNode {
|
||||||
if (change) {
|
if (change) {
|
||||||
this.app.updateShapes([change])
|
this.app.updateShapes([change])
|
||||||
return
|
return
|
||||||
} else if (util.canCrop(shape)) {
|
} else if (util.canCrop(shape) && !this.app.isShapeOrAncestorLocked(shape)) {
|
||||||
// crop on double click
|
// crop on double click
|
||||||
this.app.mark('select and crop')
|
this.app.mark('select and crop')
|
||||||
this.app.select(info.shape?.id)
|
this.app.select(info.shape?.id)
|
||||||
|
@ -190,7 +193,7 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the shape can edit, then begin editing
|
// If the shape can edit, then begin editing
|
||||||
if (util.canEdit(shape)) {
|
if (this.shouldStartEditingShape(shape)) {
|
||||||
this.startEditingShape(shape, info)
|
this.startEditingShape(shape, info)
|
||||||
} else {
|
} else {
|
||||||
// If the shape's double click handler has not created a change,
|
// If the shape's double click handler has not created a change,
|
||||||
|
@ -212,7 +215,7 @@ export class Idle extends StateNode {
|
||||||
} else {
|
} else {
|
||||||
// If the shape's double click handler has not created a change,
|
// If the shape's double click handler has not created a change,
|
||||||
// and if the shape can edit, then begin editing the shape.
|
// and if the shape can edit, then begin editing the shape.
|
||||||
if (util.canEdit(shape)) {
|
if (this.shouldStartEditingShape(shape)) {
|
||||||
this.startEditingShape(shape, info)
|
this.startEditingShape(shape, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,12 +330,12 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldStartEditingShape(): boolean {
|
private shouldStartEditingShape(shape: TLShape | null = this.app.onlySelectedShape): boolean {
|
||||||
const { onlySelectedShape } = this.app
|
if (!shape) return false
|
||||||
if (!onlySelectedShape) return false
|
if (this.app.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return false
|
||||||
|
|
||||||
const util = this.app.getShapeUtil(onlySelectedShape)
|
const util = this.app.getShapeUtil(shape)
|
||||||
return util.canEdit(onlySelectedShape)
|
return util.canEdit(shape)
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldEnterCropMode(
|
private shouldEnterCropMode(
|
||||||
|
@ -341,6 +344,7 @@ export class Idle extends StateNode {
|
||||||
): boolean {
|
): boolean {
|
||||||
const singleShape = this.app.onlySelectedShape
|
const singleShape = this.app.onlySelectedShape
|
||||||
if (!singleShape) return false
|
if (!singleShape) return false
|
||||||
|
if (this.app.isShapeOrAncestorLocked(singleShape)) return false
|
||||||
|
|
||||||
const shapeUtil = this.app.getShapeUtil(singleShape)
|
const shapeUtil = this.app.getShapeUtil(singleShape)
|
||||||
// Should the Ctrl key be pressed to enter crop mode
|
// Should the Ctrl key be pressed to enter crop mode
|
||||||
|
@ -352,6 +356,7 @@ export class Idle extends StateNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
|
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
|
||||||
|
if (this.app.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
|
||||||
this.app.mark('editing shape')
|
this.app.mark('editing shape')
|
||||||
this.app.setEditingId(shape.id)
|
this.app.setEditingId(shape.id)
|
||||||
this.parent.transition('editing_shape', info)
|
this.parent.transition('editing_shape', info)
|
||||||
|
|
|
@ -107,7 +107,8 @@ export class ScribbleBrushing extends StateNode {
|
||||||
shape.type === 'group' ||
|
shape.type === 'group' ||
|
||||||
this.newlySelectedIds.has(shape.id) ||
|
this.newlySelectedIds.has(shape.id) ||
|
||||||
(shape.type === 'frame' &&
|
(shape.type === 'frame' &&
|
||||||
util.hitTestPoint(shape, this.app.getPointInShapeSpace(shape, originPagePoint)))
|
util.hitTestPoint(shape, this.app.getPointInShapeSpace(shape, originPagePoint))) ||
|
||||||
|
this.app.isShapeOrAncestorLocked(shape)
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ export const SelectionFg = track(function SelectionFg() {
|
||||||
let bounds = app.selectionBounds
|
let bounds = app.selectionBounds
|
||||||
const shapes = app.selectedShapes
|
const shapes = app.selectedShapes
|
||||||
const onlyShape = shapes.length === 1 ? shapes[0] : null
|
const onlyShape = shapes.length === 1 ? shapes[0] : null
|
||||||
|
const isLockedShape = onlyShape && app.isShapeOrAncestorLocked(onlyShape)
|
||||||
|
|
||||||
// if all shapes have an expandBy for the selection outline, we can expand by the l
|
// if all shapes have an expandBy for the selection outline, we can expand by the l
|
||||||
const expandOutlineBy = onlyShape
|
const expandOutlineBy = onlyShape
|
||||||
|
@ -115,13 +116,15 @@ export const SelectionFg = track(function SelectionFg() {
|
||||||
!isCoarsePointer &&
|
!isCoarsePointer &&
|
||||||
!(isTinyX || isTinyY) &&
|
!(isTinyX || isTinyY) &&
|
||||||
(shouldDisplayControls || showCropHandles) &&
|
(shouldDisplayControls || showCropHandles) &&
|
||||||
(onlyShape ? !app.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true)
|
(onlyShape ? !app.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) &&
|
||||||
|
!isLockedShape
|
||||||
|
|
||||||
const showMobileRotateHandle =
|
const showMobileRotateHandle =
|
||||||
isCoarsePointer &&
|
isCoarsePointer &&
|
||||||
(!isSmallX || !isSmallY) &&
|
(!isSmallX || !isSmallY) &&
|
||||||
(shouldDisplayControls || showCropHandles) &&
|
(shouldDisplayControls || showCropHandles) &&
|
||||||
(onlyShape ? !app.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true)
|
(onlyShape ? !app.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) &&
|
||||||
|
!isLockedShape
|
||||||
|
|
||||||
const showResizeHandles =
|
const showResizeHandles =
|
||||||
shouldDisplayControls &&
|
shouldDisplayControls &&
|
||||||
|
@ -129,7 +132,8 @@ export const SelectionFg = track(function SelectionFg() {
|
||||||
? app.getShapeUtil(onlyShape).canResize(onlyShape) &&
|
? app.getShapeUtil(onlyShape).canResize(onlyShape) &&
|
||||||
!app.getShapeUtil(onlyShape).hideResizeHandles(onlyShape)
|
!app.getShapeUtil(onlyShape).hideResizeHandles(onlyShape)
|
||||||
: true) &&
|
: true) &&
|
||||||
!showCropHandles
|
!showCropHandles &&
|
||||||
|
!isLockedShape
|
||||||
|
|
||||||
const hideAlternateCornerHandles = isTinyX || isTinyY
|
const hideAlternateCornerHandles = isTinyX || isTinyY
|
||||||
const showOnlyOneHandle = isTinyX && isTinyY
|
const showOnlyOneHandle = isTinyX && isTinyY
|
||||||
|
|
|
@ -1,15 +1,175 @@
|
||||||
// import { TestApp } from '../TestApp'
|
import { createCustomShapeId } from '@tldraw/tlschema'
|
||||||
|
import { TestApp } from '../TestApp'
|
||||||
|
let app: TestApp
|
||||||
|
|
||||||
// let app: TestApp
|
const ids = {
|
||||||
|
lockedShapeA: createCustomShapeId('boxA'),
|
||||||
|
unlockedShapeA: createCustomShapeId('boxB'),
|
||||||
|
unlockedShapeB: createCustomShapeId('boxC'),
|
||||||
|
lockedShapeB: createCustomShapeId('boxD'),
|
||||||
|
lockedGroup: createCustomShapeId('lockedGroup'),
|
||||||
|
groupedBoxA: createCustomShapeId('grouppedBoxA'),
|
||||||
|
groupedBoxB: createCustomShapeId('grouppedBoxB'),
|
||||||
|
lockedFrame: createCustomShapeId('lockedFrame'),
|
||||||
|
}
|
||||||
|
|
||||||
// beforeEach(() => {
|
beforeEach(() => {
|
||||||
// app = new TestApp()
|
app = new TestApp()
|
||||||
// })
|
app.selectAll()
|
||||||
|
app.deleteShapes()
|
||||||
|
app.createShapes([
|
||||||
|
{
|
||||||
|
id: ids.lockedShapeA,
|
||||||
|
type: 'geo',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
isLocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.lockedShapeB,
|
||||||
|
type: 'geo',
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
isLocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.unlockedShapeA,
|
||||||
|
type: 'geo',
|
||||||
|
x: 200,
|
||||||
|
y: 200,
|
||||||
|
isLocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.unlockedShapeB,
|
||||||
|
type: 'geo',
|
||||||
|
x: 300,
|
||||||
|
y: 300,
|
||||||
|
isLocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.lockedGroup,
|
||||||
|
type: 'group',
|
||||||
|
x: 800,
|
||||||
|
y: 800,
|
||||||
|
isLocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.groupedBoxA,
|
||||||
|
type: 'geo',
|
||||||
|
x: 1000,
|
||||||
|
y: 1000,
|
||||||
|
parentId: ids.lockedGroup,
|
||||||
|
isLocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.groupedBoxB,
|
||||||
|
type: 'geo',
|
||||||
|
x: 1200,
|
||||||
|
y: 1200,
|
||||||
|
parentId: ids.lockedGroup,
|
||||||
|
isLocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ids.lockedFrame,
|
||||||
|
type: 'frame',
|
||||||
|
x: 1600,
|
||||||
|
y: 1600,
|
||||||
|
isLocked: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
describe('Locking', () => {
|
describe('Locking', () => {
|
||||||
it.todo('Locks all selected shapes if the selection includes any unlocked shapes')
|
it('Can lock shapes', () => {
|
||||||
|
app.setSelectedIds([ids.unlockedShapeA])
|
||||||
|
app.toggleLock()
|
||||||
|
expect(app.getShapeById(ids.unlockedShapeA)!.isLocked).toBe(true)
|
||||||
|
// Locking deselects the shape
|
||||||
|
expect(app.selectedIds).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Locked shapes', () => {
|
||||||
|
it('Cannot be deleted', () => {
|
||||||
|
const numberOfShapesBefore = app.shapesArray.length
|
||||||
|
app.deleteShapes([ids.lockedShapeA])
|
||||||
|
expect(app.shapesArray.length).toBe(numberOfShapesBefore)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be changed', () => {
|
||||||
|
const xBefore = app.getShapeById(ids.lockedShapeA)!.x
|
||||||
|
app.updateShapes([{ id: ids.lockedShapeA, type: 'geo', x: 100 }])
|
||||||
|
expect(app.getShapeById(ids.lockedShapeA)!.x).toBe(xBefore)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be moved', () => {
|
||||||
|
const shape = app.getShapeById(ids.lockedShapeA)
|
||||||
|
app.pointerDown(150, 150, { target: 'shape', shape })
|
||||||
|
app.expectToBeIn('select.idle')
|
||||||
|
|
||||||
|
app.pointerMove(10, 10)
|
||||||
|
app.expectToBeIn('select.idle')
|
||||||
|
|
||||||
|
app.pointerUp()
|
||||||
|
app.expectToBeIn('select.idle')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be selected with select all', () => {
|
||||||
|
app.selectAll()
|
||||||
|
expect(app.selectedIds).toEqual([ids.unlockedShapeA, ids.unlockedShapeB])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be selected by clicking', () => {
|
||||||
|
const shape = app.getShapeById(ids.lockedShapeA)!
|
||||||
|
|
||||||
|
app
|
||||||
|
.pointerDown(10, 10, { target: 'shape', shape })
|
||||||
|
.expectToBeIn('select.idle')
|
||||||
|
.pointerUp()
|
||||||
|
.expectToBeIn('select.idle')
|
||||||
|
expect(app.selectedIds).not.toContain(shape.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be edited', () => {
|
||||||
|
const shape = app.getShapeById(ids.lockedShapeA)!
|
||||||
|
const shapeCount = app.shapesArray.length
|
||||||
|
|
||||||
|
// We create a new shape and we edit that one
|
||||||
|
app.doubleClick(10, 10, { target: 'shape', shape }).expectToBeIn('select.editing_shape')
|
||||||
|
expect(app.shapesArray.length).toBe(shapeCount + 1)
|
||||||
|
expect(app.selectedIds).not.toContain(shape.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cannot be grouped', () => {
|
||||||
|
const shapeCount = app.shapesArray.length
|
||||||
|
const parentBefore = app.getShapeById(ids.lockedShapeA)!.parentId
|
||||||
|
|
||||||
|
app.groupShapes([ids.lockedShapeA, ids.unlockedShapeA, ids.unlockedShapeB])
|
||||||
|
expect(app.shapesArray.length).toBe(shapeCount + 1)
|
||||||
|
|
||||||
|
const parentAfter = app.getShapeById(ids.lockedShapeA)!.parentId
|
||||||
|
expect(parentAfter).toBe(parentBefore)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Locked frames do not accept new shapes', () => {
|
||||||
|
const frame = app.getShapeById(ids.lockedFrame)!
|
||||||
|
const frameUtil = app.getShapeUtil(frame)
|
||||||
|
|
||||||
|
expect(frameUtil.canReceiveNewChildrenOfType(frame, 'box')).toBe(false)
|
||||||
|
const shape = app.getShapeById(ids.lockedShapeA)!
|
||||||
|
expect(frameUtil.canDropShapes(frame, [shape])).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Unlocking', () => {
|
describe('Unlocking', () => {
|
||||||
it.todo('Unlocks all selected shapes if the selection includes only locked shapes')
|
it('Can unlock shapes', () => {
|
||||||
|
app.setSelectedIds([ids.lockedShapeA, ids.lockedShapeB])
|
||||||
|
let lockedStatus = [ids.lockedShapeA, ids.lockedShapeB].map(
|
||||||
|
(id) => app.getShapeById(id)!.isLocked
|
||||||
|
)
|
||||||
|
expect(lockedStatus).toStrictEqual([true, true])
|
||||||
|
app.toggleLock()
|
||||||
|
lockedStatus = [ids.lockedShapeA, ids.lockedShapeB].map((id) => app.getShapeById(id)!.isLocked)
|
||||||
|
expect(lockedStatus).toStrictEqual([false, false])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -24,7 +24,14 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
|
|
||||||
const contextMenuSchema = useContextMenuSchema()
|
const contextMenuSchema = useContextMenuSchema()
|
||||||
const [_, handleOpenChange] = useMenuIsOpen('context menu')
|
const cb = (isOpen: boolean) => {
|
||||||
|
if (isOpen) return
|
||||||
|
if (shouldDeselect(app)) {
|
||||||
|
app.setSelectedIds([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, handleOpenChange] = useMenuIsOpen('context menu', cb)
|
||||||
|
|
||||||
// If every item in the menu is readonly, then we don't want to show the menu
|
// If every item in the menu is readonly, then we don't want to show the menu
|
||||||
const isReadonly = useReadonly()
|
const isReadonly = useReadonly()
|
||||||
|
@ -53,6 +60,12 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldDeselect(app: App) {
|
||||||
|
const { onlySelectedShape } = app
|
||||||
|
if (!onlySelectedShape) return false
|
||||||
|
return app.isShapeOrAncestorLocked(onlySelectedShape)
|
||||||
|
}
|
||||||
|
|
||||||
function ContextMenuContent() {
|
function ContextMenuContent() {
|
||||||
const app = useApp()
|
const app = useApp()
|
||||||
const msg = useTranslation()
|
const msg = useTranslation()
|
||||||
|
|
|
@ -925,6 +925,16 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
||||||
app.zoomToContent()
|
app.zoomToContent()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'toggle-lock',
|
||||||
|
label: 'action.toggle-lock',
|
||||||
|
readonlyOk: false,
|
||||||
|
kbd: '!$l',
|
||||||
|
onSelect(source) {
|
||||||
|
trackEvent('toggle-lock', { source })
|
||||||
|
app.toggleLock()
|
||||||
|
},
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
|
|
|
@ -89,19 +89,22 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
|
||||||
const allowUngroup = useAllowUngroup()
|
const allowUngroup = useAllowUngroup()
|
||||||
const hasClipboardWrite = Boolean(window.navigator.clipboard?.write)
|
const hasClipboardWrite = Boolean(window.navigator.clipboard?.write)
|
||||||
const showEditLink = useHasLinkShapeSelected()
|
const showEditLink = useHasLinkShapeSelected()
|
||||||
|
const { onlySelectedShape } = app
|
||||||
|
const isShapeLocked = onlySelectedShape && app.isShapeOrAncestorLocked(onlySelectedShape)
|
||||||
|
|
||||||
const contextMenuSchema = useMemo<MenuSchema>(() => {
|
const contextMenuSchema = useMemo<MenuSchema>(() => {
|
||||||
let contextMenuSchema: ContextMenuSchemaContextType = compactMenuItems([
|
let contextMenuSchema: ContextMenuSchemaContextType = compactMenuItems([
|
||||||
menuGroup(
|
menuGroup(
|
||||||
'selection',
|
'selection',
|
||||||
oneEmbedSelected && menuItem(actions['open-embed-link']),
|
oneEmbedSelected && menuItem(actions['open-embed-link']),
|
||||||
oneEmbedSelected && menuItem(actions['convert-to-bookmark']),
|
oneEmbedSelected && !isShapeLocked && menuItem(actions['convert-to-bookmark']),
|
||||||
oneEmbeddableBookmarkSelected && menuItem(actions['convert-to-embed']),
|
oneEmbeddableBookmarkSelected && menuItem(actions['convert-to-embed']),
|
||||||
showAutoSizeToggle && menuItem(actions['toggle-auto-size']),
|
showAutoSizeToggle && menuItem(actions['toggle-auto-size']),
|
||||||
showEditLink && menuItem(actions['edit-link']),
|
showEditLink && !isShapeLocked && menuItem(actions['edit-link']),
|
||||||
oneSelected && menuItem(actions['duplicate']),
|
oneSelected && !isShapeLocked && menuItem(actions['duplicate']),
|
||||||
allowGroup && menuItem(actions['group']),
|
allowGroup && !isShapeLocked && menuItem(actions['group']),
|
||||||
allowUngroup && menuItem(actions['ungroup'])
|
allowUngroup && !isShapeLocked && menuItem(actions['ungroup']),
|
||||||
|
oneSelected && menuItem(actions['toggle-lock'])
|
||||||
),
|
),
|
||||||
menuGroup(
|
menuGroup(
|
||||||
'modify',
|
'modify',
|
||||||
|
@ -132,6 +135,7 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
|
||||||
menuItem(actions['stretch-vertical'])
|
menuItem(actions['stretch-vertical'])
|
||||||
),
|
),
|
||||||
onlyFlippableShapeSelected &&
|
onlyFlippableShapeSelected &&
|
||||||
|
!isShapeLocked &&
|
||||||
menuGroup(
|
menuGroup(
|
||||||
'flip',
|
'flip',
|
||||||
menuItem(actions['flip-horizontal']),
|
menuItem(actions['flip-horizontal']),
|
||||||
|
@ -146,6 +150,7 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
oneSelected &&
|
oneSelected &&
|
||||||
|
!isShapeLocked &&
|
||||||
menuSubmenu(
|
menuSubmenu(
|
||||||
'reorder',
|
'reorder',
|
||||||
'context-menu.reorder',
|
'context-menu.reorder',
|
||||||
|
@ -157,11 +162,11 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
|
||||||
menuItem(actions['send-to-back'])
|
menuItem(actions['send-to-back'])
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
oneSelected && menuCustom('MOVE_TO_PAGE_MENU', { readonlyOk: false })
|
oneSelected && !isShapeLocked && menuCustom('MOVE_TO_PAGE_MENU', { readonlyOk: false })
|
||||||
),
|
),
|
||||||
menuGroup(
|
menuGroup(
|
||||||
'clipboard-group',
|
'clipboard-group',
|
||||||
oneSelected && menuItem(actions['cut']),
|
oneSelected && !isShapeLocked && menuItem(actions['cut']),
|
||||||
oneSelected && menuItem(actions['copy']),
|
oneSelected && menuItem(actions['copy']),
|
||||||
showMenuPaste && menuItem(actions['paste'])
|
showMenuPaste && menuItem(actions['paste'])
|
||||||
),
|
),
|
||||||
|
@ -203,7 +208,7 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
|
||||||
menuItem(actions['select-all']),
|
menuItem(actions['select-all']),
|
||||||
oneSelected && menuItem(actions['select-none'])
|
oneSelected && menuItem(actions['select-none'])
|
||||||
),
|
),
|
||||||
oneSelected && menuGroup('delete-group', menuItem(actions['delete'])),
|
oneSelected && !isShapeLocked && menuGroup('delete-group', menuItem(actions['delete'])),
|
||||||
])
|
])
|
||||||
|
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
|
@ -237,6 +242,7 @@ export const ContextMenuSchemaProvider = track(function ContextMenuSchemaProvide
|
||||||
oneEmbedSelected,
|
oneEmbedSelected,
|
||||||
oneEmbeddableBookmarkSelected,
|
oneEmbeddableBookmarkSelected,
|
||||||
isTransparentBg,
|
isTransparentBg,
|
||||||
|
isShapeLocked,
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -77,6 +77,7 @@ export interface TLUiEventMap {
|
||||||
'toggle-dark-mode': null
|
'toggle-dark-mode': null
|
||||||
'toggle-focus-mode': null
|
'toggle-focus-mode': null
|
||||||
'toggle-debug-mode': null
|
'toggle-debug-mode': null
|
||||||
|
'toggle-lock': null
|
||||||
'toggle-reduce-motion': null
|
'toggle-reduce-motion': null
|
||||||
'exit-pen-mode': null
|
'exit-pen-mode': null
|
||||||
'stop-following': null
|
'stop-following': null
|
||||||
|
|
|
@ -83,6 +83,7 @@ export type TLTranslationKey =
|
||||||
| 'action.toggle-focus-mode'
|
| 'action.toggle-focus-mode'
|
||||||
| 'action.toggle-grid.menu'
|
| 'action.toggle-grid.menu'
|
||||||
| 'action.toggle-grid'
|
| 'action.toggle-grid'
|
||||||
|
| 'action.toggle-lock'
|
||||||
| 'action.toggle-snap-mode.menu'
|
| 'action.toggle-snap-mode.menu'
|
||||||
| 'action.toggle-snap-mode'
|
| 'action.toggle-snap-mode'
|
||||||
| 'action.toggle-tool-lock.menu'
|
| 'action.toggle-tool-lock.menu'
|
||||||
|
|
|
@ -83,6 +83,7 @@ export const DEFAULT_TRANSLATION = {
|
||||||
'action.toggle-focus-mode': 'Toggle focus mode',
|
'action.toggle-focus-mode': 'Toggle focus mode',
|
||||||
'action.toggle-grid.menu': 'Show grid',
|
'action.toggle-grid.menu': 'Show grid',
|
||||||
'action.toggle-grid': 'Toggle grid',
|
'action.toggle-grid': 'Toggle grid',
|
||||||
|
'action.toggle-lock': 'Lock / Unlock',
|
||||||
'action.toggle-snap-mode.menu': 'Always snap',
|
'action.toggle-snap-mode.menu': 'Always snap',
|
||||||
'action.toggle-snap-mode': 'Toggle always snap',
|
'action.toggle-snap-mode': 'Toggle always snap',
|
||||||
'action.toggle-tool-lock.menu': 'Tool lock',
|
'action.toggle-tool-lock.menu': 'Tool lock',
|
||||||
|
|
Loading…
Reference in a new issue