Skip to content
Open
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
23 changes: 23 additions & 0 deletions extensions/cornerstone/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,26 @@ function commandsModule({
}
}
},
togglePassiveDisabledToolbar({ value, itemId, toolGroupIds = ['mpr'] }) {
const toolName = itemId || value;

toolGroupIds.forEach(toolGroupId => {
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
if (!toolGroup || !toolGroup.hasTool(toolName)) {
return;
}

const currentMode = toolGroup.getToolOptions(toolName).mode;
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
Comment on lines +1054 to +1055
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Active mode excluded from isOn check (same as evaluator)

This mirrors the omission in the evaluator: if Crosshairs is somehow in Active mode (keyboard shortcut or another command), isOn will be false, and clicking the toggle button will call setToolPassive instead of setToolDisabled, leaving crosshairs active without feedback.

The same fix applies here:

Suggested change
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled ||
currentMode === Enums.ToolModes.Active;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same reasoning as above. Active mode is out of scope for this PR and will be handled in follow-up work.


if (isOn) {
toolGroup.setToolDisabled(toolName);
} else {
toolGroup.setToolPassive(toolName);
}
});
},
setToolActiveToolbar: ({ value, itemId, toolName, toolGroupIds = [], bindings }) => {
// Sometimes it is passed as value (tools with options), sometimes as itemId (toolbar buttons)
toolName = toolName || itemId || value;
Expand Down Expand Up @@ -2639,6 +2659,9 @@ function commandsModule({
toggleActiveDisabledToolbar: {
commandFn: actions.toggleActiveDisabledToolbar,
},
togglePassiveDisabledToolbar: {
commandFn: actions.togglePassiveDisabledToolbar,
},
updateStoredPositionPresentation: {
commandFn: actions.updateStoredPositionPresentation,
},
Expand Down
47 changes: 47 additions & 0 deletions extensions/cornerstone/src/getToolbarModule.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Enums } from '@cornerstonejs/tools';
import i18n from '@ohif/i18n';
import { utils as coreUtils } from '@ohif/core';
import { utils } from '@ohif/ui-next';
import { ViewportDataOverlayMenuWrapper } from './components/ViewportDataOverlaySettingMenu/ViewportDataOverlayMenuWrapper';
import { ViewportOrientationMenuWrapper } from './components/ViewportOrientationMenu/ViewportOrientationMenuWrapper';
Expand All @@ -13,6 +14,8 @@ import TrackingStatus from './components/TrackingStatus/TrackingStatus';
import ViewportColorbarsContainer from './components/ViewportColorbar';
import AdvancedRenderingControls from './components/AdvancedRenderingControls';

const { ModifierKeyCodeToName } = coreUtils;

const getDisabledState = (disabledText?: string) => ({
disabled: true,
disabledText: disabledText ?? i18n.t('Buttons:Not available on the current viewport'),
Expand Down Expand Up @@ -429,6 +432,50 @@ export default function getToolbarModule({ servicesManager, extensionManager }:
};
},
},
{
name: 'evaluate.cornerstoneTool.crosshairToggle',
evaluate: ({ viewportId, button, disabledText }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}

const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName)) {
return getDisabledState(disabledText);
}

const currentMode = toolGroup.getToolOptions(toolName).mode;
// Matches the isOn check in toggleCrosshairsToolbar (commandsModule.ts).
// Both must agree on which modes count as "on" — this evaluator uses it to show the
// toggled button state, the command uses it to decide whether to disable or enable.
// If adding Active mode support (e.g. modifier key), update both.
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
Comment on lines +453 to +454
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Active mode excluded from isOn check

The isOn detection uses Passive || Enabled but omits Active. If Crosshairs is activated through an alternative path (e.g., a keyboard binding or another command that calls setToolActive), the button will appear un-toggled even though the tool is visually active in the viewports. The corresponding command logic (line 1054–1055 in commandsModule.ts) shares the same omission, so clicking the button when Crosshairs is already Active would try to enable Passive mode again instead of disabling it, leaving the crosshairs on.

For consistency with toggleActiveDisabledToolbar, consider including Active:

Suggested change
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled ||
currentMode === Enums.ToolModes.Active;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The omission of Active is intentional.

See the PR message above - this implementation scopes the toolbar button to a pure on/off toggle (Passive ↔ Disabled). The crosshair tool is never set to Active through the toolbar. Active mode will be handled separately at the viewport level in @sedghi's upcoming work via a modifier key interaction. The toolbar button intentionally stays in its toggled appearance during that interaction. Any changes to support Active mode would be part of that follow-up work.


if (isOn) {
const toolInstance = toolGroup.getToolInstance(toolName);
const jumpOnClick = toolInstance?.configuration?.jumpOnClick;
if (jumpOnClick?.enabled && jumpOnClick.modifierKey != null) {
const modifierName = ModifierKeyCodeToName[jumpOnClick.modifierKey] || 'Modifier';
return {
disabled: false,
isActive: false,
isToggled: true,
icon: 'tool-crosshair-checked',
tooltip: `Press ${modifierName} + Click to jump`,
};
}
}

return {
disabled: false,
isActive: false,
isToggled: isOn,
icon: isOn ? 'tool-crosshair-checked' : 'tool-crosshair',
};
},
Comment on lines +436 to +477
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Duplicated isOn detection logic

The isOn evaluation (Passive || Enabled) is copy-pasted identically into both evaluate.cornerstoneTool.crosshairToggle (here) and toggleCrosshairsToolbar in commandsModule.ts. When one changes (e.g., adding Active to the check), the other must be updated manually.

Consider extracting this into a shared helper, or at minimum leaving a comment cross-referencing the two sites so they stay in sync.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The two checks should stay in sync. I added cross-reference comments in both locations explaining that they must agree on which modes count as "on," and noting that both need updating if Active mode support is added.

},
{
name: 'evaluate.action',
evaluate: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ interface HotkeyDefinitions {
}

function UserPreferencesModalDefault({ hide }: { hide: () => void }) {
const { hotkeysManager } = useSystem();
const { hotkeysManager, servicesManager } = useSystem();
const { t, i18n: i18nextInstance } = useTranslation('UserPreferencesModal');
const { customizationService, toolbarService, viewportGridService } =
servicesManager.services;
const rawMouseShortcuts =
customizationService.getCustomization('ohif.hotkeyBindings.mouseShortcuts') || [];
const initialMouseShortcuts = rawMouseShortcuts.map(shortcut => ({
...shortcut,
keys: typeof shortcut.keys === 'function' ? shortcut.keys() : shortcut.keys,
}));

const { hotkeyDefinitions = {}, hotkeyDefaults = {} } = hotkeysManager;

Expand Down Expand Up @@ -54,6 +62,7 @@ function UserPreferencesModalDefault({ hide }: { hide: () => void }) {
const [state, setState] = useState({
hotkeyDefinitions: initialHotkeyDefinitions,
languageValue: currentLanguage.value,
mouseShortcuts: initialMouseShortcuts,
});

const onLanguageChangeHandler = (value: string) => {
Expand All @@ -73,11 +82,21 @@ function UserPreferencesModalDefault({ hide }: { hide: () => void }) {
}));
};

const onMouseShortcutChangeHandler = (index: number, newKeys: string) => {
setState(prev => ({
...prev,
mouseShortcuts: prev.mouseShortcuts.map((shortcut, i) =>
i === index ? { ...shortcut, keys: newKeys } : shortcut
),
}));
};

const onResetHandler = () => {
setState(state => ({
...state,
languageValue: defaultLanguage.value,
hotkeyDefinitions: resolvedHotkeyDefaults,
mouseShortcuts: initialMouseShortcuts,
}));

hotkeysManager.restoreDefaultBindings();
Expand Down Expand Up @@ -152,6 +171,23 @@ function UserPreferencesModalDefault({ hide }: { hide: () => void }) {
</Select>
</div>

{state.mouseShortcuts.length > 0 && (
<>
<UserPreferencesModal.SubHeading>{t('Mouse Shortcuts')}</UserPreferencesModal.SubHeading>
<UserPreferencesModal.HotkeysGrid>
{state.mouseShortcuts.map((shortcut, index) => (
<UserPreferencesModal.MouseShortcut
key={index}
label={t(shortcut.label)}
value={shortcut.keys}
onChange={newKeys => onMouseShortcutChangeHandler(index, newKeys)}
hotkeys={hotkeysModule}
/>
))}
</UserPreferencesModal.HotkeysGrid>
</>
)}

<UserPreferencesModal.SubHeading>{t('Hotkeys')}</UserPreferencesModal.SubHeading>
<UserPreferencesModal.HotkeysGrid>
{Object.entries(state.hotkeyDefinitions).map(([id, definition]) => (
Expand Down Expand Up @@ -186,11 +222,20 @@ function UserPreferencesModalDefault({ hide }: { hide: () => void }) {
onClick={() => {
if (state.languageValue !== currentLanguage.value) {
i18n.changeLanguage(state.languageValue);
// Force page reload after language change to ensure all translations are applied
window.location.reload();
return; // Exit early since we're reloading
return;
}
hotkeysManager.setHotkeys(state.hotkeyDefinitions);

for (const shortcut of state.mouseShortcuts) {
shortcut.onChange?.(shortcut.keys);
}

if (state.mouseShortcuts.length > 0) {
const viewportId = viewportGridService.getActiveViewportId();
toolbarService.refreshToolbarState({ viewportId });
}

hotkeysModule.stopRecord();
hotkeysModule.unpause();
hide();
Expand Down
29 changes: 29 additions & 0 deletions modes/basic/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,35 @@ export function onModeEnter({
// Init Default and SR ToolGroups
initToolGroups(extensionManager, toolGroupService, commandsManager);

customizationService.setCustomizations({
'ohif.hotkeyBindings.mouseShortcuts': [
{
label: 'Crosshairs Jump to Click',
keys: () => {
const config = toolGroupService.getToolConfiguration('mpr', 'Crosshairs');
const code = config?.jumpOnClick?.modifierKey;
return code != null
? utils.ModifierKeyCodeToName[code]?.toLowerCase()
: undefined;
},
onChange: (newKeys: string) => {
const config = toolGroupService.getToolConfiguration('mpr', 'Crosshairs');
if (!config) {
return;
}
toolGroupService.setToolConfiguration('mpr', 'Crosshairs', {
...config,
jumpOnClick: {
...config.jumpOnClick,
modifierKey:
utils.ModifierKeyNameToCode[newKeys] ?? config.jumpOnClick?.modifierKey,
},
});
},
},
],
});

toolbarService.register(this.toolbarButtons);

for (const [key, section] of Object.entries(this.toolbarSections)) {
Expand Down
6 changes: 5 additions & 1 deletion modes/basic/src/initToolGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,15 @@ function initMPRToolGroup(extensionManager, toolGroupService, commandsManager) {
xOffset: 0.95,
yOffset: 0.05,
},
disableOnPassive: true,
disableOnPassive: false,
autoPan: {
enabled: false,
panSize: 10,
},
jumpOnClick: {
enabled: true,
modifierKey: Enums.KeyboardBindings.Ctrl,
},
getReferenceLineColor: viewportId => {
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
const viewportOptions = viewportInfo?.viewportOptions;
Expand Down
5 changes: 3 additions & 2 deletions modes/basic/src/toolbarButtons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,14 +647,15 @@ const toolbarButtons: Button[] = [
type: 'tool',
icon: 'tool-crosshair',
label: i18n.t('Buttons:Crosshairs'),
tooltip: i18n.t('Buttons:Click to toggle on or off'),
commands: {
commandName: 'setToolActiveToolbar',
commandName: 'togglePassiveDisabledToolbar',
commandOptions: {
toolGroupIds: ['mpr'],
},
},
evaluate: {
name: 'evaluate.cornerstoneTool',
name: 'evaluate.cornerstoneTool.crosshairToggle',
disabledText: i18n.t('Buttons:Select an MPR viewport to enable this tool'),
},
},
Expand Down
5 changes: 5 additions & 0 deletions platform/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { structuredCloneWithFunctions } from './structuredCloneWithFunctions';
import { buildButtonCommands } from './buildButtonCommands';

import { downloadBlob, downloadUrl, downloadCsv, downloadDicom } from './downloadBlob';
import { ModifierKeyCodeToName, ModifierKeyNameToCode } from './modifierKeyUtils';

// Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager.
Expand Down Expand Up @@ -105,6 +106,8 @@ const utils = {
downloadUrl,
downloadCsv,
downloadDicom,
ModifierKeyCodeToName,
ModifierKeyNameToCode,
};

export {
Expand Down Expand Up @@ -147,6 +150,8 @@ export {
downloadUrl,
downloadCsv,
downloadDicom,
ModifierKeyCodeToName,
ModifierKeyNameToCode,
};

export default utils;
26 changes: 26 additions & 0 deletions platform/core/src/utils/modifierKeyUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Bidirectional mapping between modifier-key names and their numeric key-code
* values. These values mirror the `KeyboardBindings` enum from
* `@cornerstonejs/tools` (packages/tools/src/enums/ToolBindings.ts).
*
* @ohif/core cannot depend on @cornerstonejs/tools directly, so the canonical
* values are duplicated here. If KeyboardBindings ever changes, update this
* file to match.
*/

/** Modifier key-code → display name (e.g. 17 → 'Ctrl') */
const ModifierKeyCodeToName: Record<number, string> = {
16: 'Shift',
17: 'Ctrl',
18: 'Alt',
91: 'Cmd',
};

/** Lowercase key name → modifier key-code (e.g. 'ctrl' → 17) */
const ModifierKeyNameToCode: Record<string, number> = Object.fromEntries(
Object.entries(ModifierKeyCodeToName).map(([code, name]) => [name.toLowerCase(), Number(code)])
);
// 'cmd' → 91 but preferences store 'meta' for the Meta/Cmd key
ModifierKeyNameToCode.meta = ModifierKeyNameToCode.cmd;

export { ModifierKeyCodeToName, ModifierKeyNameToCode };
3 changes: 3 additions & 0 deletions platform/ui-next/src/components/Icons/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import {
ToolCobbAngle,
ToolCreateThreshold,
ToolCrosshair,
ToolCrosshairChecked,
ToolDicomTagBrowser,
ToolFlipHorizontal,
ToolFreehandPolygon,
Expand Down Expand Up @@ -436,6 +437,7 @@ export const Icons = {
ToolCobbAngle,
ToolCreateThreshold,
ToolCrosshair,
ToolCrosshairChecked,
ToolDicomTagBrowser,
ToolFlipHorizontal,
ToolFreehandPolygon,
Expand Down Expand Up @@ -703,6 +705,7 @@ export const Icons = {
'tool-cobb-angle': (props: IconProps) => ToolCobbAngle(props),
'tool-create-threshold': (props: IconProps) => ToolCreateThreshold(props),
'tool-crosshair': (props: IconProps) => ToolCrosshair(props),
'tool-crosshair-checked': (props: IconProps) => ToolCrosshairChecked(props),
'dicom-tag-browser': (props: IconProps) => ToolDicomTagBrowser(props),
'tool-flip-horizontal': (props: IconProps) => ToolFlipHorizontal(props),
'tool-freehand-polygon': (props: IconProps) => ToolFreehandPolygon(props),
Expand Down
Loading
Loading