Integrating hotkeys with commands

This commit is contained in:
Bruno Alves de Faria 2017-04-25 20:15:19 -03:00 committed by Erik Ziegler
parent 199115a9d3
commit 1a0729336b
7 changed files with 102 additions and 89 deletions

View File

@ -102,7 +102,7 @@ Template.toolbarSection.helpers({
iconClasses: 'fa fa-trash'
});
var buttonData = [];
const buttonData = [];
buttonData.push({
id: 'zoom',
@ -175,7 +175,8 @@ Template.toolbarSection.helpers({
id: 'toggleCinePlay',
title: 'Toggle CINE Play',
classes: 'imageViewerCommand',
buttonTemplateName: 'playClipButton'
buttonTemplateName: 'playClipButton',
active: OHIF.viewerbase.viewportUtils.isPlaying
});
buttonData.push({
@ -183,7 +184,8 @@ Template.toolbarSection.helpers({
title: 'CINE',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-youtube-play',
disableFunction: OHIF.viewerbase.viewportUtils.hasMultipleFrames
disableFunction: OHIF.viewerbase.viewportUtils.hasMultipleFrames,
active: () => $('#cineDialog').is(':visible')
});
}

View File

@ -1,5 +1,4 @@
import { Tracker } from 'meteor/tracker';
import { Session } from 'meteor/session';
import { ReactiveVar } from 'meteor/reactive-var';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
@ -7,11 +6,8 @@ export class CommandsManager {
constructor() {
this.contexts = {};
Tracker.autorun(() => {
const currentContextName = OHIF.context.get();
OHIF.log.info(currentContextName);
// TODO: initialize hotkeys context
});
// Enable reactivity by storing the last executed command
this.last = new ReactiveVar('');
}
getContext(contextName) {
@ -80,7 +76,12 @@ export class CommandsManager {
return OHIF.log.warn(`No action was defined for command "${command}"`);
} else {
const result = action(params);
Session.set('lastCommand', command);
if (this.last.get() === command) {
this.last.dep.changed();
} else {
this.last.set(command);
}
return result;
}
}

View File

@ -9,39 +9,48 @@ export class HotkeysContext {
extend(definitions={}) {
if (typeof definitions !== 'object') return;
this.destroy();
Object.assign(this.definitions, definitions);
this.initialize();
Object.keys(definitions).forEach(command => {
const hotkey = definitions[command];
this.unregister(command);
this.register(command, hotkey);
this.definitions[command] = hotkey;
});
}
register(command, hotkey) {
if (!hotkey) {
return OHIF.log.warn(`No hotkey was defined for "${command}"`);
}
if (!command) {
return OHIF.log.warn(`No command was defined for hotkey "${hotkey}"`);
}
const bindingKey = `keydown.hotkey.${this.name}.${command}`;
const bind = hotkey => $(document).bind(bindingKey, hotkey, event => {
if (!this.enabled.get()) return;
OHIF.commands.run(command);
});
if (hotkey instanceof Array) {
hotkey.forEach(hotkey => bind(hotkey));
} else {
bind(hotkey);
}
}
unregister(command) {
const bindingKey = `keydown.hotkey.${this.name}.${command}`;
if (this.definitions[command]) {
$(document).unbind(bindingKey);
delete this.definitions[command];
}
}
initialize() {
Object.keys(this.definitions).forEach(definitionKey => {
const { hotkey, action, disabled } = this.definitions[definitionKey];
let message;
if (!hotkey) {
message = `No hotkey was defined for "${definitionKey}"`;
} else if (!action) {
message = `No action was defined for "${definitionKey}" using "${hotkey}" hotkey`;
}
if (message) {
return OHIF.log.warn(message);
}
const bindHotkey = hotkey => $(document).bind(`keydown.hotkey.${this.name}`, hotkey, event => {
if (!this.enabled.get()) return;
if (typeof disabled !== undefined) {
if ((typeof disabled === 'function' && disabled()) || disabled) return;
}
action(event);
});
if (hotkey instanceof Array) {
hotkey.forEach(hotkey => bindHotkey(hotkey));
} else {
bindHotkey(hotkey);
}
Object.keys(this.definitions).forEach(command => {
const hotkey = this.definitions[command];
this.register(command, hotkey);
});
}

View File

@ -1,4 +1,6 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { OHIF } from 'meteor/ohif:core';
import { HotkeysContext } from 'meteor/ohif:hotkeys/client/classes/HotkeysContext';
export class HotkeysManager {
@ -6,6 +8,11 @@ export class HotkeysManager {
this.contexts = {};
this.currentContextName = null;
this.enabled = new ReactiveVar(true);
Tracker.autorun(() => {
const contextName = OHIF.context.get();
this.switchToContext(contextName);
});
}
disable() {
@ -24,7 +31,7 @@ export class HotkeysManager {
return this.getContext(this.currentContextName);
}
setContext(contextName, contextDefinitions) {
set(contextName, contextDefinitions, extend=false) {
const enabled = this.enabled;
const context = new HotkeysContext(contextName, contextDefinitions, enabled);
const currentContext = this.getCurrentContext();
@ -36,6 +43,16 @@ export class HotkeysManager {
this.contexts[contextName] = context;
}
register(contextName, command, hotkey) {
if (!command || !hotkey) return;
const context = this.getContext(contextName);
if (!context) {
this.set(contextName, {});
}
context.register(command, hotkey);
}
unsetContext(contextName) {
if (contextName === this.currentContextName) {
this.getCurrentContext().destroy();

View File

@ -16,7 +16,6 @@ Package.onUse(function(api) {
]);
// OHIF dependencies
api.use('ohif:core');
api.use('ohif:commands');
// Main module definition

View File

@ -7,7 +7,7 @@ Template.toolbarSectionButton.onCreated(() => {
const instance = Template.instance();
instance.isActive = activeToolId => {
// TODO: Find a way to prevent the 'flash' after a click, but before this helper runs
OHIF.commands.last.dep.depend();
const instance = Template.instance();
const subTools = instance.data.subTools;
const currentId = instance.data.id;
@ -15,9 +15,10 @@ Template.toolbarSectionButton.onCreated(() => {
const isSubTool = subTools && _.findWhere(subTools, { id: activeToolId });
const activeCommandButtons = Session.get('ToolManagerActiveCommandButtons') || [];
const isActiveCommandButton = activeCommandButtons.indexOf(instance.data.id) !== -1;
const isActive = typeof instance.data.active === 'function' && instance.data.active();
// Check if the current tool, a sub tool or a command button is active
return isCurrentTool || isSubTool || isActiveCommandButton;
return isActive || isCurrentTool || isSubTool || isActiveCommandButton;
};
instance.getActiveToolSubProperty = (propertyName, activeToolId) => {
@ -36,19 +37,34 @@ Template.toolbarSectionButton.onCreated(() => {
instance.autorun(computation => {
// Get the last executed command
const lastCommand = Session.get('lastCommand');
const lastCommand = OHIF.commands.last.get();
// Prevent running this computation on its first run
if (computation.firstRun) return;
// Stop here if it's not the last command or if it's already an active tool
const { id } = instance.data;
if (lastCommand !== id || instance.isActive(id)) return;
const activeToolId = OHIF.viewerbase.toolManager.getActiveTool();
if (lastCommand !== id || instance.isActive(activeToolId)) return;
// Add an active class to a button for 100ms to give the impression the button was pressed
const $button = instance.$('.toolbarSectionButton:first');
$button.addClass('active');
setTimeout(() => $button.removeClass('active'), 100);
const flashButton = $element => {
$element.addClass('active');
setTimeout(() => {
if ($element.hasClass('expandable') && $element.find('.toolbarSectionButton.active').length) return;
$element.removeClass('active');
}, 100);
};
// Flash the active button
const $button = instance.$('.toolbarSectionButton').first();
flashButton($button);
// Flash the parent button as well in case of this button is inside a drawer
const $parentButton = $button.closest('.toolbarSectionButton.expandable');
if ($parentButton.length) {
flashButton($parentButton);
}
});
});

View File

@ -37,7 +37,7 @@ Meteor.startup(function() {
flipH: 'H',
wwwc: 'W',
zoom: 'Z',
cinePlay: 'SPACE',
toggleCinePlay: 'SPACE',
rotateR: 'R',
rotateL: 'L',
toggleOverlayTags: 'SHIFT',
@ -71,6 +71,7 @@ Meteor.startup(function() {
angle: 'Angle',
dragProbe: 'Probe',
ellipticalRoi: 'Ellipse',
rectangleRoi: 'Rectangle',
magnify: 'Magnify',
annotate: 'Annotate',
stackScroll: 'Stack Scroll',
@ -98,7 +99,10 @@ Meteor.startup(function() {
flipV: 'Flip vertically',
rotateR: 'Rotate right',
rotateL: 'Rotate left',
toggleCinePlay: 'Play/Pause'
toggleCinePlay: 'Play/Pause',
toggleCineDialog: 'CINE dialog',
resetViewport: 'Reset',
clearTools: 'Clear'
});
// Functions to register the preset switching commands
@ -172,10 +176,6 @@ Meteor.startup(function() {
const $dicomTags = $('.imageViewerViewportOverlay .dicomTag');
$dicomTags.toggle($dicomTags.eq(0).css('display') === 'none');
}
},
cineDialog: {
name: 'Toggle CINE',
action: viewportUtils.toggleCineDialog()
}
}, true);
@ -203,45 +203,14 @@ function setOHIFHotkeys(hotkeys) {
OHIF.viewer.hotkeys = hotkeys;
}
/**
* Add an active class to a button for 100ms only
* to give the impressiont the button was pressed.
* This is for tools that don't keep the button "pressed"
* all the time the tool is active.
*
* @param button DOM Element for the button to be "flashed"
*/
function flashButton(button) {
if (!button) {
return;
}
button.classList.add('active');
setTimeout(() => {
button.classList.remove('active');
}, 100);
}
/**
* Binds all hotkeys keydown events to the tasks defined in
* OHIF.viewer.hotkeys or a given param
* @param {Object} hotkeys hotkey and task mapping (not required). If not given, uses OHIF.viewer.hotkeys
*/
function enableHotkeys(hotkeys) {
const viewerHotkeys = hotkeys || OHIF.viewer.hotkeys;
const definitions = {};
Object.keys(viewerHotkeys).forEach(definition => {
const hotkey = viewerHotkeys[definition];
const action = OHIF.viewer.hotkeyFunctions[definition];
definitions[definition] = {
hotkey,
action
};
});
OHIF.hotkeys.setContext('viewer', definitions);
OHIF.hotkeys.switchToContext('viewer');
const definitions = hotkeys || OHIF.viewer.hotkeys;
OHIF.hotkeys.set('viewer', definitions);
OHIF.context.set('viewer');
}