fix(hotkeys): fix the hotkey manager to not save the defaults (#3162)

This commit is contained in:
Bill Wallace 2023-02-10 14:30:56 -05:00 committed by GitHub
parent 4558540357
commit a97fb2fc9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 149 additions and 60 deletions

View File

@ -0,0 +1,8 @@
export default interface Hotkey {
commandName: string;
commandOptions?: Record<string, unknown>;
context?: string;
keys: string[];
label: string;
isEditable?: boolean;
}

View File

@ -1,6 +1,9 @@
import objectHash from 'object-hash'; import objectHash from 'object-hash';
import log from './../log.js'; import log from '../log.js';
import { hotkeys } from '../utils'; import { hotkeys } from '../utils';
import isequal from 'lodash.isequal';
import Hotkey from './Hotkey';
import ServicesManager from '../services/ServicesManager';
/** /**
* *
@ -13,6 +16,8 @@ import { hotkeys } from '../utils';
*/ */
export class HotkeysManager { export class HotkeysManager {
private _servicesManager: ServicesManager;
constructor(commandsManager, servicesManager) { constructor(commandsManager, servicesManager) {
this.hotkeyDefinitions = {}; this.hotkeyDefinitions = {};
this.hotkeyDefaults = []; this.hotkeyDefaults = [];
@ -59,11 +64,17 @@ export class HotkeysManager {
* *
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions * @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
*/ */
setHotkeys(hotkeyDefinitions = []) { setHotkeys(hotkeyDefinitions = [], key = 'hotkey-definitions') {
try { try {
const definitions = this.getValidDefinitions(hotkeyDefinitions); const definitions = this.getValidDefinitions(hotkeyDefinitions);
if (isequal(definitions, this.hotkeyDefaults)) {
console.log('hotkeys REMOVING unused definition', key);
localStorage.removeItem(key);
} else {
console.log('hotkeys setting local storage', key);
localStorage.setItem(key, JSON.stringify(definitions));
}
definitions.forEach(definition => this.registerHotkeys(definition)); definitions.forEach(definition => this.registerHotkeys(definition));
localStorage.setItem('hotkey-definitions', JSON.stringify(definitions));
} catch (error) { } catch (error) {
const { UINotificationService } = this._servicesManager.services; const { UINotificationService } = this._servicesManager.services;
UINotificationService.show({ UINotificationService.show({
@ -165,7 +176,14 @@ export class HotkeysManager {
* @returns {undefined} * @returns {undefined}
*/ */
registerHotkeys( registerHotkeys(
{ commandName, commandOptions = {}, keys, label, isEditable } = {}, {
commandName,
commandOptions = {},
context,
keys,
label,
isEditable,
}: Hotkey = {},
extension extension
) { ) {
if (!commandName) { if (!commandName) {
@ -181,9 +199,9 @@ export class HotkeysManager {
if (previouslyRegisteredDefinition) { if (previouslyRegisteredDefinition) {
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys; const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
this._unbindHotkeys(commandName, previouslyRegisteredKeys); this._unbindHotkeys(commandName, previouslyRegisteredKeys);
log.info( // log.info(
`[hotkeys] Unbinding ${commandName} with ${options} options from ${previouslyRegisteredKeys}` // `[hotkeys] Unbinding ${commandName} with ${options} options from ${previouslyRegisteredKeys}`
); // );
} }
// Set definition & bind // Set definition & bind
@ -194,10 +212,11 @@ export class HotkeysManager {
label, label,
isEditable, isEditable,
}; };
this._bindHotkeys(commandName, commandOptions, keys); this._bindHotkeys(commandName, commandOptions, context, keys);
log.info( // log.info(
`[hotkeys] Binding ${commandName} with ${options} options to ${keys}` // `[hotkeys] Binding ${commandName} with ${options} from ${context ||
); // 'default'} options to ${keys}`
// );
} }
/** /**
@ -226,7 +245,7 @@ export class HotkeysManager {
* @param {string[]} keys - One or more key combinations that should trigger command * @param {string[]} keys - One or more key combinations that should trigger command
* @returns {undefined} * @returns {undefined}
*/ */
_bindHotkeys(commandName, commandOptions = {}, keys) { _bindHotkeys(commandName, commandOptions = {}, context, keys) {
const isKeyDefined = keys === '' || keys === undefined; const isKeyDefined = keys === '' || keys === undefined;
if (isKeyDefined) { if (isKeyDefined) {
return; return;
@ -238,7 +257,11 @@ export class HotkeysManager {
hotkeys.bind(combinedKeys, evt => { hotkeys.bind(combinedKeys, evt => {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
this._commandsManager.runCommand(commandName, { evt, ...commandOptions }); this._commandsManager.runCommand(
commandName,
{ evt, ...commandOptions },
context
);
}); });
} }

View File

@ -1,10 +1,10 @@
import ExtensionManager from './ExtensionManager.js'; import ExtensionManager from './ExtensionManager';
import MODULE_TYPES from './MODULE_TYPES.js'; import MODULE_TYPES from './MODULE_TYPES';
import log from './../log.js'; import log from './../log.js';
jest.mock('./../log.js'); jest.mock('./../log.js');
describe('ExtensionManager.js', () => { describe('ExtensionManager.ts', () => {
let extensionManager, commandsManager, servicesManager, appConfig; let extensionManager, commandsManager, servicesManager, appConfig;
beforeEach(() => { beforeEach(() => {
@ -238,6 +238,9 @@ describe('ExtensionManager.js', () => {
getCustomizationModule: () => { getCustomizationModule: () => {
return [{}]; return [{}];
}, },
getStateSyncModule: () => {
return [{}];
},
}; };
await extensionManager.registerExtension(extension); await extensionManager.registerExtension(extension);
@ -245,7 +248,7 @@ describe('ExtensionManager.js', () => {
// Registers 1 module per module type // Registers 1 module per module type
Object.keys(extensionManager.modules).forEach(moduleType => { Object.keys(extensionManager.modules).forEach(moduleType => {
const modulesForType = extensionManager.modules[moduleType]; const modulesForType = extensionManager.modules[moduleType];
console.log('moduleType', moduleType);
expect(modulesForType.length).toBe(1); expect(modulesForType.length).toBe(1);
}); });
}); });

View File

@ -1,13 +1,59 @@
import MODULE_TYPES from './MODULE_TYPES.js'; import MODULE_TYPES from './MODULE_TYPES';
import log from './../log.js'; import log from '../log';
import { AppConfig } from '../types/AppConfig';
import { ServicesManager } from '../services';
import { HotkeysManager, CommandsManager } from '../classes';
/**
* This is the arguments given to create the extension.
*/
export interface ExtensionConstructor {
servicesManager: ServicesManager;
commandsManager: CommandsManager;
hotkeysManager: HotkeysManager;
appConfig: AppConfig;
}
/**
* The configuration of an extension.
* This uses type as the extension manager only knows that the configuration
* is an object of some sort, and doesn't know anything else about it.
*/
export type ExtensionConfiguration = Record<string, unknown>;
/**
* The parameters passed to the extension.
*/
export interface ExtensionParams extends ExtensionConstructor {
extensionManager: ExtensionManager;
configuration?: ExtensionConfiguration;
}
/**
* The type of an actual extension instance.
* This is an interface as it declares possible calls, but extensions can
* have more values than this.
*/
export interface Extension {
preRegistration?: (p: ExtensionParams) => void;
}
export type ExtensionRegister = {
id: string;
create: (p: ExtensionParams) => Extension;
};
export default class ExtensionManager { export default class ExtensionManager {
private _commandsManager: CommandsManager;
private _servicesManager: ServicesManager;
private _hotkeysManager: HotkeysManager;
constructor({ constructor({
commandsManager, commandsManager,
servicesManager, servicesManager,
hotkeysManager, hotkeysManager,
appConfig = {}, appConfig = {},
}) { }: ExtensionConstructor) {
this.modules = {}; this.modules = {};
this.registeredExtensionIds = []; this.registeredExtensionIds = [];
this.moduleTypeNames = Object.values(MODULE_TYPES); this.moduleTypeNames = Object.values(MODULE_TYPES);
@ -28,11 +74,17 @@ export default class ExtensionManager {
this.activeDataSource = undefined; this.activeDataSource = undefined;
} }
setActiveDataSource(dataSourceName) { public setActiveDataSource(dataSourceName: string): void {
this.activeDataSource = dataSourceName; this.activeDataSource = dataSourceName;
} }
onModeEnter() { /**
* Calls all the services and extension on mode enters.
* The service onModeEnter is called first
* Then registered extensions onModeEnter is called
* This is supposed to setup the extension for a standard entry.
*/
public onModeEnter(): void {
const { const {
registeredExtensionIds, registeredExtensionIds,
_servicesManager, _servicesManager,
@ -61,7 +113,7 @@ export default class ExtensionManager {
}); });
} }
onModeExit() { public onModeExit(): void {
const { const {
registeredExtensionIds, registeredExtensionIds,
_servicesManager, _servicesManager,
@ -97,7 +149,13 @@ export default class ExtensionManager {
* *
* @param {Object[]} extensions - Array of extensions * @param {Object[]} extensions - Array of extensions
*/ */
registerExtensions = async (extensions, dataSources = []) => { public registerExtensions = async (
extensions: (
| ExtensionRegister
| [ExtensionRegister, ExtensionConfiguration]
)[],
dataSources: unknown[] = []
): Promise<void> => {
// Todo: we ideally should be able to run registrations in parallel // Todo: we ideally should be able to run registrations in parallel
// but currently since some extensions need to be registered before // but currently since some extensions need to be registered before
// others, we need to run them sequentially. We need a postInit hook // others, we need to run them sequentially. We need a postInit hook
@ -127,16 +185,16 @@ export default class ExtensionManager {
* @param {Object} extension * @param {Object} extension
* @param {Object} configuration * @param {Object} configuration
*/ */
registerExtension = async ( public registerExtension = async (
extension, extension: ExtensionRegister,
configuration = {}, configuration = {},
dataSources = [] dataSources = []
) => { ): Promise<void> => {
if (!extension) { if (!extension) {
throw new Error('Attempting to register a null/undefined extension.'); throw new Error('Attempting to register a null/undefined extension.');
} }
let extensionId = extension.id; const extensionId = extension.id;
if (!extensionId) { if (!extensionId) {
// Note: Mode framework cannot function without IDs. // Note: Mode framework cannot function without IDs.
@ -203,6 +261,7 @@ export default class ExtensionManager {
case MODULE_TYPES.CONTEXT: case MODULE_TYPES.CONTEXT:
case MODULE_TYPES.LAYOUT_TEMPLATE: case MODULE_TYPES.LAYOUT_TEMPLATE:
case MODULE_TYPES.CUSTOMIZATION: case MODULE_TYPES.CUSTOMIZATION:
case MODULE_TYPES.STATE_SYNC:
case MODULE_TYPES.UTILITY: case MODULE_TYPES.UTILITY:
// Default for most extension points, // Default for most extension points,
// Just adds each entry ready for consumption by mode. // Just adds each entry ready for consumption by mode.

View File

@ -1,9 +1,11 @@
import ExtensionManager from './ExtensionManager'; import ExtensionManager from './ExtensionManager';
import MODULE_TYPES from './MODULE_TYPES.js'; import MODULE_TYPES from './MODULE_TYPES';
export default { const DEFAULT_EXPORTS = {
ExtensionManager, ExtensionManager,
MODULE_TYPES, MODULE_TYPES,
}; };
export default DEFAULT_EXPORTS;
export { ExtensionManager, MODULE_TYPES }; export { ExtensionManager, MODULE_TYPES };

View File

@ -0,0 +1,7 @@
import Hotkey from '../classes/Hotkey';
export interface AppConfig {
extensions?: string[];
defaultDataSourceName?: string;
hotkeys?: Record<string, Hotkey> | Hotkey[];
}

View File

@ -3,5 +3,3 @@ export interface Command {
commandOptions?: Record<string, unknown>; commandOptions?: Record<string, unknown>;
context?: string; context?: string;
} }
export default Command;

View File

@ -1,5 +1,4 @@
/** /**
* Just a function that consumes a single argument, with no return. * Just a function that consumes a single argument, with no return.
*/ */
type Consumer = (props: Record<string, unknown>) => void; export type Consumer = (props: Record<string, unknown>) => void;
export default Consumer;

View File

@ -24,4 +24,5 @@ export default interface Services {
viewportGridService?: ViewportGridService; viewportGridService?: ViewportGridService;
syncGroupService?: Record<string, unknown>; syncGroupService?: Record<string, unknown>;
cornerstoneCacheService?: Record<string, unknown>; cornerstoneCacheService?: Record<string, unknown>;
segmentationService?: Record<string, unknown>;
} }

View File

@ -1,4 +1,6 @@
/** Defines a typescript type for study metadata */ /** Defines a typescript interface for study metadata.
* This defines the types for when using study metadata as interfaces.
*/
export interface PatientMetadata extends Record<string, unknown> { export interface PatientMetadata extends Record<string, unknown> {
PatientName?: string; PatientName?: string;
@ -20,4 +22,3 @@ export interface InstanceMetadata extends SeriesMetadata {
readonly SOPInstanceUID: string; readonly SOPInstanceUID: string;
InstanceNumber?: string | number; InstanceNumber?: string | number;
} }
export default StudyMetadata;

View File

@ -1,29 +1,17 @@
import { import * as Extensions from '../extensions/ExtensionManager';
StudyMetadata,
SeriesMetadata,
InstanceMetadata,
} from './StudyMetadata';
import Consumer from './Consumer';
import { ExtensionManager } from '../extensions';
import { CustomizationService, PubSubService } from '../services';
import * as HangingProtocol from './HangingProtocol'; import * as HangingProtocol from './HangingProtocol';
import Command from './Command';
import Services from './Services'; import Services from './Services';
import { CommandsManager } from '../classes'; import Hotkey from '../classes/Hotkey';
export * from '../services/CustomizationService/types'; export * from '../services/CustomizationService/types';
// Separate out some generic types
export * from './AppConfig';
export * from './Consumer';
export * from './Command';
export * from './StudyMetadata';
export type { /**
ExtensionManager, * Export the types used within the various services and managers, but
HangingProtocol, * not the services/managers themselves, which are exported at the top level.
StudyMetadata, */
SeriesMetadata, export { Extensions, HangingProtocol, Services, Hotkey };
InstanceMetadata,
Consumer,
PubSubService,
CustomizationService,
Command,
Services,
CommandsManager,
};