Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyboard service ⌨️ #136

Merged
merged 4 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/whiteboard/toolbar/TheToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ function toggleActive(idx: number) {
v-for="(tool, idx) in tools"
:key="idx"
:is="tool.component"
:is-active="tool.isActive"
:is-disabled="tool.isDisabled"
:isActive="tool.isActive"
:isDisabled="tool.isDisabled"
@click="toggleActive(idx)"
/>
<ColorPicker />
Expand Down
18 changes: 18 additions & 0 deletions src/services/canvas/CanvasService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@ import HistoryService from '../history/HistoryService'
import ShapeDrawnEvent from '@/store/history/event/ShapeDrawn'
import ShapeDeletedEvent from '@/store/history/event/ShapeDeleted'
import ShapeUpdatedEvent from '@/store/history/event/ShapeUpdated'
import { useToolbarStore } from '@/store/ToolbarStore'
import KeyboardService from '../keyboard/KeyboardService'

export default class CanvasService {
constructor() {
const canvasKeyboard = KeyboardService.get('canvas')
canvasKeyboard.registerCallback(['Delete'], () =>
this.deleteSelectedShapes()
)
}

public getShapeById(id: string) {
const canvasStore = useCanvasStore()
return canvasStore.getShapeById(id)
Expand Down Expand Up @@ -51,4 +60,13 @@ export default class CanvasService {
const canvasStore = useCanvasStore()
canvasStore.currentlyDrawnShape = shape
}

private deleteSelectedShapes() {
const toolbarStore = useToolbarStore()
HistoryService.startAggregating()
toolbarStore.foreachSelectedShape((shape) => {
this.removeShapeById(shape.id)
})
HistoryService.stopAggregating()
}
}
10 changes: 10 additions & 0 deletions src/services/history/HistoryService.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { useHistoryStore } from '@/store/history/HistoryStore'
import type IHistoryEvent from './IHistoryEvent'
import AggregateEvent from '@/store/history/event/AggregateEvent'
import KeyboardService from '../keyboard/KeyboardService'

class HistoryService {
private aggregating = false
private aggragateBuffer: Array<IHistoryEvent> = []

constructor() {
const canvasKeyboard = KeyboardService.get('canvas')
canvasKeyboard.registerCallback(['Control', 'z'], () => this.undo())
canvasKeyboard.registerCallback(['Control', 'Shift', 'Z'], () =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The z key is now, probably by mistake, in uppercase.

this.redo()
)
canvasKeyboard.registerCallback(['Control', 'y'], () => this.redo())
}

public undo() {
const historyStore = useHistoryStore()
const lastEvent = historyStore.popEvent()
Expand Down
53 changes: 53 additions & 0 deletions src/services/keyboard/KeyMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { InvalidArguments } from '@/utils/exceptions/InvalidArguments'

export type KeyCallback = () => void
type KeyTree = Map<string, KeyTree> | KeyCallback

export default class KeyMap {
private keyTreeRoot: KeyTree = new Map<string, KeyTree>()

public get(keySequence: string[]) {
return this.getLeaf(keySequence, this.keyTreeRoot)
}

public set(keySequence: string[], callback: KeyCallback) {
if (keySequence.length < 1) return
this.keyTreeRoot = this.setLeaf(keySequence, this.keyTreeRoot, callback)
}

private getLeaf(keySequence: string[], keyTree: KeyTree): KeyCallback | null {
if (keySequence.length < 1) {
/* Reached bottom of sequence */
if (keyTree instanceof Map) return null
return keyTree
}

if (!(keyTree instanceof Map)) {
/* Reached bottom of the tree */
return null
}

const firstKey = keySequence[0]
if (!keyTree.has(firstKey)) return null
return this.getLeaf(keySequence.slice(1), keyTree.get(firstKey)!)
}

private setLeaf(
keySequence: string[],
keyTree: KeyTree,
leaf: KeyCallback
): KeyTree {
if (keySequence.length < 1)
/* Reached bottom of the sequence */
return leaf

if (!(keyTree instanceof Map))
/* Reached bottom of the tree */
throw new InvalidArguments('KeyMap.set - conflicting shortcuts')

const firstKey = keySequence[0]
const recSequence = keySequence.slice(1)
const recTree = keyTree.has(firstKey) ? keyTree.get(firstKey)! : new Map()
return keyTree.set(firstKey, this.setLeaf(recSequence, recTree, leaf))
}
}
65 changes: 65 additions & 0 deletions src/services/keyboard/KeyboardService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Logger, { type ILogger } from 'js-logger'
import KeyMap, { type KeyCallback } from './KeyMap'

type KeyboardServiceMap = Map<string, KeyboardService>

class KeyboardService {
private logger: ILogger
private keyTree: KeyMap = new KeyMap()

constructor(keyspace: string) {
this.logger = Logger.get(`KeyboardService[${keyspace}]`)
}

public registerCallback(keySequence: string[], callback: KeyCallback) {
this.keyTree.set(keySequence, callback)
}

public handleClick(keySequence: string[]) {
const callback = this.keyTree.get(keySequence)
if (callback) callback()
}
}

class GlobalKeyboardService {
private logger = Logger.get('KeyboardService')
private keyspaces: KeyboardServiceMap = new Map<string, KeyboardService>()
private selectedKeyspace: string | null = null

constructor() {
const clickHandler = (event: KeyboardEvent) => this.handleClick(event)
window.removeEventListener('keydown', clickHandler)
window.addEventListener('keydown', clickHandler)
}

public get(keyspace: string) {
if (this.keyspaces.has(keyspace)) return this.keyspaces.get(keyspace)!

const newKeyboardService = new KeyboardService(keyspace)
this.keyspaces.set(keyspace, newKeyboardService)
this.logger.debug(`New keyspace added: ${keyspace}`)

return newKeyboardService
}

public activateKeyspace(keyspace: string | null) {
const previousKeyspace = this.selectedKeyspace
this.selectedKeyspace = keyspace
this.logger.debug(`Activated keyspace ${keyspace}`)
return previousKeyspace
}

public handleClick(event: KeyboardEvent) {
const keySequence: Array<string> = []
/* For now support only combinations with ctrl and ctrl + shift */
if (event.ctrlKey && event.key != 'Control') keySequence.push('Control')
if (event.shiftKey && event.key != 'Shift') keySequence.push('Shift')
keySequence.push(event.key)

if (!this.selectedKeyspace) return
const service = this.get(this.selectedKeyspace)
service.handleClick(keySequence)
}
}

export default new GlobalKeyboardService()
2 changes: 2 additions & 0 deletions src/views/TheBoard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import TheSidebar from '@/components/whiteboard/sidebar/TheSidebar.vue'
import { useSidebarStore } from '@/store/SidebarStore'
import PageLoader from '@/components/loading/PageLoader.vue'
import TheMagic from '@/components/whiteboard/magic/TheMagic.vue'
import KeyboardService from '@/services/keyboard/KeyboardService'

const logger = Logger.get('MainWhiteboard.vue')

Expand Down Expand Up @@ -47,6 +48,7 @@ watch(
function handleCanvasReady() {
initState.canvasReady = true
logger.debug('Canvas ready indication received!')
KeyboardService.activateKeyspace('canvas')
}

function handleMagicReady() {
Expand Down
Loading