Skip to content
This repository has been archived by the owner on Jun 14, 2021. It is now read-only.

Commit

Permalink
Support copy / cut / paste via application menu
Browse files Browse the repository at this point in the history
EventEmitterを使わないと、「今何が選択されているか」をEditorStoreに突っ込んでおかないと
アプリケーションメニューのコールバック側から知ることができないが
そのように対応すると結構大掛かりな仕掛けが必要になるので
EventEmitterからイベントを投げて、その時フォーカスされてる要素に処理を移譲するようにした
  • Loading branch information
hanakla committed Sep 18, 2018
1 parent b8576aa commit 1c73891
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 47 deletions.
2 changes: 1 addition & 1 deletion packages/delir/domain/Editor/actions.ts
Expand Up @@ -19,6 +19,6 @@ export const EditorActions = {
addMessageAction: action<{ id: string, title?: string, level: 'info' | 'error', message: string, detail?: string }>(),
removeMessageAction: action<{ id: string }>(),
seekPreviewFrameAction: action<{ frame: number }>(),
setClipboardEntry: action<{entry: ClipboardEntry}>(),
setClipboardEntry: action<{ entry: ClipboardEntry }>(),
changePreferenceOpenStateAction: action<{ open: boolean }>()
}
6 changes: 3 additions & 3 deletions packages/delir/domain/Editor/operations.ts
Expand Up @@ -12,7 +12,7 @@ import EditorStore from './EditorStore'

import { EditorActions } from './actions'
import t from './operations.i18n'
import { ParameterTarget } from './types'
import { ClipboardEntry, ParameterTarget } from './types'

export type DragEntity =
| { type: 'asset', asset: Delir.Entity.Asset }
Expand Down Expand Up @@ -237,10 +237,10 @@ export const changePreferenceOpenState = operation((context, { open }: { open: b
})

//
// Clipboard
// Internal clipboard
//
export const copyEntity = operation((context, { type, entity }: {
type: 'clip'
type: ClipboardEntry['type']
entity: Delir.Entity.Clip
}) => {
context.dispatch(EditorActions.setClipboardEntry, {
Expand Down
7 changes: 1 addition & 6 deletions packages/delir/domain/Editor/types.ts
Expand Up @@ -6,12 +6,7 @@ export interface ParameterTarget {
paramName: string
}

// This is mark of don't use some instance (use clone)
type CloneOf<T extends object> = {
[K in keyof T]: T[K]
}

export interface ClipboardEntry {
type: 'clip'
entityClone: CloneOf<Delir.Entity.Clip>
entityClone: any
}
27 changes: 27 additions & 0 deletions packages/delir/utils/EventEmitter.ts
@@ -0,0 +1,27 @@
export class EventEmitter<T extends object> {
protected exclusiveEvents: string[] = []
private listeners: { [K in keyof T]: ((arg: T[K]) => void)[] } = Object.create({})

public on<K extends keyof T>(event: K, listener: (arg: T[K]) => void) {
const listeners = this.listeners[event] = (this.listeners[event] || [])

if (this.exclusiveEvents.includes(event)) {
this.listeners[event] = [listener]
} else {
listeners.push(listener)
}
}

public off<K extends keyof T>(event: K, listener: (arg: T[K]) => void) {
if (!this.listeners[event]) return

const index = this.listeners[event].findIndex(l => l === listener)
if (index === -1) return
this.listeners[event].splice(index, 1)
}

public emit<K extends keyof T>(event: K, arg: T[K]) {
if (!this.listeners[event]) return
this.listeners[event].forEach(listener => listener(arg))
}
}
2 changes: 1 addition & 1 deletion packages/delir/utils/makeMousetrapHandler.ts
@@ -1,5 +1,5 @@
// Mousetrap is fired event on Input element when _belongsTo is satisfied
export const makeMousetrapHandler = (fn: (e: KeyboardEvent, combo: string) => void) => {
export const makeMousetrapIgnoreInputHandler = (fn: (e: KeyboardEvent, combo: string) => void) => {
return (e: KeyboardEvent, combo: string) => {
const target: HTMLElement = (e.target || e.srcElement) as HTMLElement

Expand Down
64 changes: 51 additions & 13 deletions packages/delir/views/AppMenu/AppMenu.tsx
Expand Up @@ -2,12 +2,12 @@ import { connectToStores, ContextProp, withComponentContext } from '@ragg/fleur-
import * as Electron from 'electron'
import { remote } from 'electron'
import * as React from 'react'
import * as Platform from '../../utils/platform'

import EditorStore, { EditorState } from '../../domain/Editor/EditorStore'
import * as EditorOps from '../../domain/Editor/operations'
import * as HistoryOps from '../../domain/History/operations'
import * as AboutModal from '../../modules/AboutModal'
import * as Platform from '../../utils/platform'
import { GlobalEvent, GlobalEvents } from '../AppView/GlobalEvents'

import t from './AppMenu.i18n'

Expand All @@ -17,6 +17,11 @@ interface ConnectedProps {

type Props = ConnectedProps & ContextProp

const isSelectionInputElement = (el: Element) => {
return (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement)
&& (el.selectionStart !== el.selectionEnd)
}

export default withComponentContext(connectToStores([EditorStore], (context) => ({
editor: context.getStore(EditorStore).getState()
}))(class AppMenu extends React.Component<Props> {
Expand All @@ -38,14 +43,6 @@ export default withComponentContext(connectToStores([EditorStore], (context) =>
return null
}

private openAbout = () => {
AboutModal.show()
}

private handleOpenPreference = () => {
this.props.context.executeOperation(EditorOps.changePreferenceOpenState, { open: true })
}

private setApplicationMenu()
{
const {context} = this.props
Expand Down Expand Up @@ -176,15 +173,18 @@ export default withComponentContext(connectToStores([EditorStore], (context) =>
},
{
label: t('edit.cut'),
role: 'cut'
accelerator: 'CmdOrCtrl+X',
click: this.handleCut,
},
{
label: t('edit.copy'),
role: 'copy'
accelerator: 'CmdOrCtrl+C',
click: this.handleCopy,
},
{
label: t('edit.paste'),
role: 'paste'
accelerator: 'CmdOrCtrl+V',
click: this.handlePaste,
},
{
label: t('edit.selectAll'),
Expand Down Expand Up @@ -234,4 +234,42 @@ export default withComponentContext(connectToStores([EditorStore], (context) =>

remote.Menu.setApplicationMenu(remote.Menu.buildFromTemplate(menu))
}

private openAbout = () => {
AboutModal.show()
}

private handleOpenPreference = () => {
this.props.context.executeOperation(EditorOps.changePreferenceOpenState, { open: true })
}

private handleCopy = () => {
const {activeElement} = document

if (isSelectionInputElement(activeElement)) {
document.execCommand('copy')
} else {
GlobalEvents.emit(GlobalEvent.copyViaApplicationMenu, {})
}
}

private handleCut = () => {
const {activeElement} = document

if (isSelectionInputElement(activeElement)) {
document.execCommand('cut')
} else {
GlobalEvents.emit(GlobalEvent.cutViaApplicationMenu, {})
}
}

private handlePaste = () => {
const {activeElement} = document

if (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement) {
document.execCommand('paste')
} else {
GlobalEvents.emit(GlobalEvent.pasteViaApplicationMenu, {})
}
}
}))
6 changes: 3 additions & 3 deletions packages/delir/views/AppView/AppView.tsx
Expand Up @@ -2,7 +2,7 @@ import { connectToStores, ContextProp, withComponentContext } from '@ragg/fleur-
import * as Mousetrap from 'mousetrap'
import * as React from 'react'
import { CSSTransitionGroup } from 'react-transition-group'
import { makeMousetrapHandler } from '../../utils/makeMousetrapHandler'
import { makeMousetrapIgnoreInputHandler } from '../../utils/makeMousetrapHandler'

import EditorStore, { EditorState } from '../../domain/Editor/EditorStore'
import * as EditorOps from '../../domain/Editor/operations'
Expand Down Expand Up @@ -111,13 +111,13 @@ export default withComponentContext(connectToStores([EditorStore], (context) =>
}

// tslint:disable-next-line: member-ordering
private handleShortCutUndo = makeMousetrapHandler((e: KeyboardEvent) => {
private handleShortCutUndo = makeMousetrapIgnoreInputHandler((e: KeyboardEvent) => {
e.preventDefault()
this.props.context.executeOperation(HistoryOps.doUndo, {})
})

// tslint:disable-next-line: member-ordering
private handleShortCutRedo = makeMousetrapHandler((e: KeyboardEvent) => {
private handleShortCutRedo = makeMousetrapIgnoreInputHandler((e: KeyboardEvent) => {
e.preventDefault()
this.props.context.executeOperation(HistoryOps.doRedo, {})
})
Expand Down
21 changes: 21 additions & 0 deletions packages/delir/views/AppView/GlobalEvents.ts
@@ -0,0 +1,21 @@
import { EventEmitter } from '../../utils/EventEmitter'

export enum GlobalEvent {
copyViaApplicationMenu = 'copy',
cutViaApplicationMenu = 'cut',
pasteViaApplicationMenu = 'paste',
}

interface Events {
[GlobalEvent.copyViaApplicationMenu]: {}
[GlobalEvent.cutViaApplicationMenu]: {}
[GlobalEvent.pasteViaApplicationMenu]: {}
}

export const GlobalEvents = new class extends EventEmitter<Events> {
protected exclusiveEvents = [
GlobalEvent.copyViaApplicationMenu,
GlobalEvent.cutViaApplicationMenu,
GlobalEvent.pasteViaApplicationMenu,
]
}()
25 changes: 13 additions & 12 deletions packages/delir/views/Timeline/_Clip.tsx
Expand Up @@ -10,6 +10,7 @@ import * as EditorOps from '../../domain/Editor/operations'
import * as ProjectOps from '../../domain/Project/operations'
import { ContextMenu, MenuItem, MenuItemOption } from '../components/ContextMenu'

import { GlobalEvent, GlobalEvents } from '../AppView/GlobalEvents'
import t from './_Clip.i18n'
import * as s from './Clip.styl'

Expand All @@ -30,13 +31,10 @@ interface ConnectedProps {
type Props = OwnProps & ConnectedProps & ContextProp

export default withComponentContext(class Clip extends React.Component<Props> {
private trap: InstanceType<typeof Mousetrap>
private clipRoot = React.createRef<HTMLDivElement>()

public componentDidMount() {
this.trap = new Mousetrap(this.clipRoot.current!)
this.trap.bind(['command+c', 'ctrl+c'], this.handleCopyClip)
this.trap.bind(['command+x', 'ctrl+x'], this.handleCutClip)
}

public render()
Expand All @@ -61,12 +59,10 @@ export default withComponentContext(class Clip extends React.Component<Props> {
onDragStart={this.handleDragStart}
onDragStop={this.handleDragEnd}
onResizeStop={this.handleResizeEnd}
onMouseDown={this.handleClick}
tabIndex={-1}
>
<div
ref={this.clipRoot}
onClick={this.handleClick}
tabIndex={-1}
>
<div ref={this.clipRoot}>
<ContextMenu>
<MenuItem label={t('contextMenu.seekToHeadOfClip')} onClick={this.handleSeekToHeadOfClip} />
<MenuItem label={t('contextMenu.effect')}>
Expand All @@ -88,8 +84,10 @@ export default withComponentContext(class Clip extends React.Component<Props> {
)
}

private handleClick = (e: React.DragEvent<HTMLDivElement>) =>
private handleClick = () =>
{
GlobalEvents.on(GlobalEvent.copyViaApplicationMenu, this.handleGlobalCopy)
GlobalEvents.on(GlobalEvent.cutViaApplicationMenu, this.handleGlobalCut)
this.props.context.executeOperation(EditorOps.changeActiveClip, { clipId: this.props.clip.id! })
}

Expand Down Expand Up @@ -133,11 +131,14 @@ export default withComponentContext(class Clip extends React.Component<Props> {
this.props.context.executeOperation(EditorOps.seekPreviewFrame, { frame: clip.placedFrame })
}

private handleCopyClip = () => {
this.props.context.executeOperation(EditorOps.copyEntity, { type: 'clip', entity: this.props.clip })
private handleGlobalCopy = () => {
this.props.context.executeOperation(EditorOps.copyEntity, {
type: 'clip',
entity: this.props.clip,
})
}

private handleCutClip = () => {
private handleGlobalCut = () => {
this.props.context.executeOperation(EditorOps.copyEntity, { type: 'clip', entity: this.props.clip })
this.props.context.executeOperation(ProjectOps.removeClip, { clipId: this.props.clip.id })
}
Expand Down
16 changes: 8 additions & 8 deletions packages/delir/views/Timeline/_ClipSpace.tsx
Expand Up @@ -13,6 +13,7 @@ import RendererStore from '../../domain/Renderer/RendererStore'
import TimePixelConversion from '../../utils/TimePixelConversion'
import { ContextMenu, MenuItem, MenuItemOption } from '../components/ContextMenu'

import { GlobalEvent, GlobalEvents } from '../AppView/GlobalEvents'
import Clip from './_Clip'
import t from './_ClipSpace.i18n'

Expand Down Expand Up @@ -46,13 +47,7 @@ export default withComponentContext(connectToStores([EditorStore, RendererStore]
dragovered: false,
}

private trap: InstanceType<typeof Mousetrap>
private root = React.createRef<HTMLDivElement>()

public componentDidMount() {
this.trap = new Mousetrap(this.root.current!)
this.trap.bind(['command+v', 'ctrl+v'], this.handlePasteClip)
}
private root = React.createRef<HTMLLIElement>()

public render()
{
Expand All @@ -69,6 +64,7 @@ export default withComponentContext(connectToStores([EditorStore, RendererStore]
})}
onDrop={this.handleOnDrop}
onMouseUp={this.handleMouseUp}
onFocus={this.handleFocus}
tabIndex={-1}
>
<ContextMenu>
Expand Down Expand Up @@ -110,6 +106,10 @@ export default withComponentContext(connectToStores([EditorStore, RendererStore]
)
}

private handleFocus = () => {
GlobalEvents.on(GlobalEvent.pasteViaApplicationMenu, this.handleGlobalPaste)
}

private handleOnDrop = (e: React.DragEvent<HTMLLIElement>) =>
{
const {dragEntity, activeComp} = this.props.editor
Expand Down Expand Up @@ -195,7 +195,7 @@ export default withComponentContext(connectToStores([EditorStore, RendererStore]
})
}

private handlePasteClip = () => {
private handleGlobalPaste = () => {
this.props.context.executeOperation(ProjectOps.pasteClipEntityIntoLayer, {
layerId: this.props.layer.id,
})
Expand Down

0 comments on commit 1c73891

Please sign in to comment.