diff --git a/src/component/hooks/useActiveSpectra.ts b/src/component/hooks/useActiveSpectra.ts index 9b0e132f2..402454340 100644 --- a/src/component/hooks/useActiveSpectra.ts +++ b/src/component/hooks/useActiveSpectra.ts @@ -4,6 +4,8 @@ import { useChartData } from '../context/ChartContext.js'; import { getActiveSpectra } from '../reducer/helper/getActiveSpectra.js'; export function useActiveSpectra() { - const state = useChartData(); - return useMemo(() => getActiveSpectra(state), [state]); + const { + view: { spectra }, + } = useChartData(); + return useMemo(() => getActiveSpectra(spectra), [spectra]); } diff --git a/src/component/hooks/useCheckToolsVisibility.ts b/src/component/hooks/useCheckToolsVisibility.ts index c5dd9aeb4..9b8855479 100644 --- a/src/component/hooks/useCheckToolsVisibility.ts +++ b/src/component/hooks/useCheckToolsVisibility.ts @@ -1,4 +1,3 @@ -import type { Info1D, Info2D } from '@zakodium/nmr-types'; import { useCallback } from 'react'; import { useChartData } from '../context/ChartContext.js'; @@ -7,102 +6,98 @@ import type { MainTool, ToolOptionItem } from '../toolbar/ToolTypes.js'; import { options } from '../toolbar/ToolTypes.js'; import useCheckExperimentalFeature from './useCheckExperimentalFeature.js'; -import useSpectrum from './useSpectrum.js'; +import { useSelectedSpectra } from './useSelectedSpectra.ts'; +import useSpectraByActiveNucleus from './useSpectraPerNucleus.ts'; -type SpectrumInfo = Info1D | Info2D; - -export interface CheckOptions { - checkSpectrumType?: boolean; - checkMode?: boolean; - extraInfoCheckParameters?: SpectrumInfo; -} - -export function useCheckToolsVisibility(): ( - toolKey: MainTool, - checkOptions?: CheckOptions, -) => boolean { +export function useCheckToolsVisibility(): (toolKey: MainTool) => boolean { const { displayerMode } = useChartData(); const preferences = usePreferences(); - const spectrum = useSpectrum(null); + const selectedSpectra = useSelectedSpectra(); + const spectra = useSpectraByActiveNucleus(); const isExperimentalFeatureActivated = useCheckExperimentalFeature(); + const toolbarButtons = preferences?.current?.display?.toolBarButtons; return useCallback( - (toolKey: MainTool, checkOptions: CheckOptions = {}) => { + (toolKey: MainTool): boolean => { const { - checkMode = true, - checkSpectrumType = true, - extraInfoCheckParameters, - } = checkOptions; - - const { spectraOptions, mode, isExperimental } = options[toolKey]; + spectraFilter, + spectraMatch = 'all', + selectedSpectra: selectionRules = { min: 1, max: 1 }, + mode, + isExperimental, + } = options[toolKey]; - // TODO: make sure preferences are not a lie and remove the optional chaining. - const flag = - preferences?.current?.display?.toolBarButtons?.[toolKey] ?? false; - - const modeFlag = - !checkMode || (checkMode && (!mode || displayerMode === mode)); - - const spectrumCheckFlag = - !checkSpectrumType || - (checkSpectrumType && checkSpectrum(spectrum, spectraOptions)); + // 1. Tool status + const flag = toolbarButtons?.[toolKey] ?? false; const isToolActivated = (flag && !isExperimental) || (isExperimental && isExperimentalFeatureActivated); - return !!( - isToolActivated && - modeFlag && - spectrumCheckFlag && - (!extraInfoCheckParameters || - checkInfo(extraInfoCheckParameters, spectrum?.info)) - ); - }, - [displayerMode, isExperimentalFeatureActivated, preferences, spectrum], - ); -} + if (!isToolActivated) return false; -function checkSpectrum( - spectrum: any, - options: ToolOptionItem['spectraOptions'], -) { - let outerConditionResult = false; + // 2. Mode check + if (mode && displayerMode !== mode) return false; - if (!options) { - return true; - } + // 3. No spectra rules, always visible + if (!spectraFilter) return true; + + // 4. Resolve spectra source + const spectraToCheck = + selectedSpectra && selectedSpectra.length > 0 + ? selectedSpectra + : spectra; - for (const option of options) { - let innerConditionFlag = true; + // 5. Selection constraints + if (selectionRules) { + const { min, max } = selectionRules; - if (option.active) { - if (spectrum) { - for (const { key, value } of option.info || []) { - if (spectrum.info[key] !== value) { - innerConditionFlag = false; - } + if (typeof min === 'number' && spectraToCheck.length < min) { + return false; + } + + if (typeof max === 'number' && spectraToCheck.length > max) { + return false; } - } else { - innerConditionFlag = false; } - } - outerConditionResult = outerConditionResult || innerConditionFlag; - } + // 6. Evaluate spectra filters + const matchCount = spectraToCheck.filter((spectrum) => + checkSpectrum(spectrum, spectraFilter), + ).length; - return outerConditionResult; + // 7. Match strategy + if (spectraMatch === 'any') { + return matchCount > 0; + } + + return matchCount === spectraToCheck.length; + }, + [ + displayerMode, + isExperimentalFeatureActivated, + toolbarButtons, + selectedSpectra, + spectra, + ], + ); } -function checkInfo(checkParameters: SpectrumInfo, data: SpectrumInfo) { - for (const key in checkParameters) { - if ( - checkParameters[key as keyof SpectrumInfo] !== - data[key as keyof SpectrumInfo] - ) { - return false; - } +function checkSpectrum( + spectrum: any, + spectraFilter: ToolOptionItem['spectraFilter'], +): boolean { + if (!spectraFilter) return true; + + for (const option of spectraFilter) { + if (!spectrum) continue; + + const infoConditionsMet = (option.info ?? []).every( + ({ key, value }) => spectrum.info[key] === value, + ); + + if (infoConditionsMet) return true; } - return true; + return false; } diff --git a/src/component/hooks/useSelectedSpectra.ts b/src/component/hooks/useSelectedSpectra.ts index 9b65321fe..ca9bcbd4a 100644 --- a/src/component/hooks/useSelectedSpectra.ts +++ b/src/component/hooks/useSelectedSpectra.ts @@ -8,7 +8,6 @@ import { useActiveSpectra } from './useActiveSpectra.ts'; export function useSelectedSpectra() { const activeSpectrum = useActiveSpectra(); const { data } = useChartData(); - return useMemo(() => { const spectra = []; diff --git a/src/component/reducer/actions/DomainActions.ts b/src/component/reducer/actions/DomainActions.ts index a4d117bb6..e89658347 100644 --- a/src/component/reducer/actions/DomainActions.ts +++ b/src/component/reducer/actions/DomainActions.ts @@ -272,8 +272,13 @@ function setDomain(draft: Draft, options?: SetDomainOptions) { } function setMode(draft: Draft) { - const { xDomains, view, data, displayerMode } = draft; - const nucleus = view.spectra.activeTab; + const { + xDomains, + view: { spectra }, + data, + displayerMode, + } = draft; + const { activeTab: nucleus } = spectra; if (displayerMode === '1D') { const spectrum = data.find( @@ -282,7 +287,7 @@ function setMode(draft: Draft) { ); draft.mode = spectrum && isFid1DSpectrum(spectrum) ? 'LTR' : 'RTL'; } else { - const activeSpectra = getActiveSpectra(draft); + const activeSpectra = getActiveSpectra(spectra); let hasFt: boolean; if (Array.isArray(activeSpectra) && activeSpectra?.length > 0) { hasFt = activeSpectra.some((spectrum) => diff --git a/src/component/reducer/actions/PeaksActions.ts b/src/component/reducer/actions/PeaksActions.ts index 5c1e13a05..970c94008 100644 --- a/src/component/reducer/actions/PeaksActions.ts +++ b/src/component/reducer/actions/PeaksActions.ts @@ -179,14 +179,18 @@ function handleAutoPeakPicking( action: AutoPeaksPickingAction, ) { const { options, defaultPeakShape } = action.payload; - - const activeSpectra = getActiveSpectra(draft); + const { + view: { spectra }, + xDomain, + toolOptions, + } = draft; + const activeSpectra = getActiveSpectra(spectra); if (!activeSpectra || activeSpectra.length === 0) { return; } - const [from, to] = draft.xDomain; + const [from, to] = xDomain; for (const activeSpectrum of activeSpectra) { const spectrum = getSpectrum(draft, activeSpectrum.index); @@ -205,8 +209,8 @@ function handleAutoPeakPicking( spectrum.peaks.values = spectrum.peaks.values.concat(peaks); } - draft.toolOptions.selectedTool = 'zoom'; - draft.toolOptions.selectedOptionPanel = null; + toolOptions.selectedTool = 'zoom'; + toolOptions.selectedOptionPanel = null; } //action diff --git a/src/component/reducer/actions/SpectraActions.ts b/src/component/reducer/actions/SpectraActions.ts index d538b588f..aaff485fe 100644 --- a/src/component/reducer/actions/SpectraActions.ts +++ b/src/component/reducer/actions/SpectraActions.ts @@ -295,14 +295,14 @@ function handleChangeActiveSpectrum( action: ChangeActiveSpectrumAction, ) { const { - view: { - spectra: { activeTab, selectReferences, activeSpectra }, - }, + view: { spectra: viewSpectra }, data, toolOptions, } = draft; + + const { activeTab, selectReferences, activeSpectra } = viewSpectra; const { modifier, id, sortedSpectra } = action.payload; - const spectra = getActiveSpectra(draft); + const spectra = getActiveSpectra(viewSpectra); //get the spectra that its nucleus match the active tab const spectraPerNucleus = getSpectraByNucleus( @@ -472,13 +472,17 @@ function resolveDeleteSpectraIDs( draft: Draft, action: DeleteSpectraAction, ): SpectraEntry[] { - const activeSpectra = getActiveSpectra(draft); + const { + view: { spectra }, + data, + } = draft; + const activeSpectra = getActiveSpectra(spectra); const { ids, spectrumSource } = action?.payload || {}; const result: SpectraEntry[] = []; if (spectrumSource) { - for (let index = 0; index < draft.data.length; index++) { - const { info, id } = draft.data[index]; + for (let index = 0; index < data.length; index++) { + const { info, id } = data[index]; if (info?.spectrumSource === spectrumSource) { result.push({ id, index }); } @@ -490,8 +494,8 @@ function resolveDeleteSpectraIDs( if (ids) { const remainingIDs = new Set(ids); - for (let index = 0; index < draft.data.length; index++) { - const { id } = draft.data[index]; + for (let index = 0; index < data.length; index++) { + const { id } = data[index]; if (remainingIDs.has(id)) { result.push({ id, index }); remainingIDs.delete(id); diff --git a/src/component/reducer/actions/ToolsActions.ts b/src/component/reducer/actions/ToolsActions.ts index c4bead2f1..32d99f94b 100644 --- a/src/component/reducer/actions/ToolsActions.ts +++ b/src/component/reducer/actions/ToolsActions.ts @@ -329,7 +329,10 @@ function handleZoom(draft: Draft, action: ZoomAction) { } case '1D': { - const activeSpectra = getActiveSpectra(draft); + const { + view: { spectra }, + } = draft; + const activeSpectra = getActiveSpectra(spectra); // Horizontal zoom in/out 1d spectra by mouse wheel if (shiftKey || isBidirectionalZoom) { @@ -612,12 +615,10 @@ function levelChangeHandler(draft: Draft, action: LevelChangeAction) { const { deltaY, altKey, invertScroll } = action.payload.options; const { data, - view: { - spectraContourLevels, - spectra: { activeTab }, - }, + view: { spectraContourLevels, spectra: viewSpectra }, } = draft; - const activeSpectra = getActiveSpectra(draft) || []; + const { activeTab } = viewSpectra; + const activeSpectra = getActiveSpectra(viewSpectra) || []; const activeSpectraObj: Record = {}; for (const activeSpectrum of activeSpectra) { diff --git a/src/component/reducer/helper/getActiveSpectra.ts b/src/component/reducer/helper/getActiveSpectra.ts index 3fc691f50..3bb674714 100644 --- a/src/component/reducer/helper/getActiveSpectra.ts +++ b/src/component/reducer/helper/getActiveSpectra.ts @@ -1,9 +1,10 @@ +import type { ViewState } from '@zakodium/nmrium-core'; import type { Draft } from 'immer'; -import type { State } from '../Reducer.js'; - -export function getActiveSpectra(state: Draft | State) { - const { activeSpectra, activeTab } = state.view.spectra; +export function getActiveSpectra( + viewSpectraState: Draft | ViewState['spectra'], +) { + const { activeSpectra, activeTab } = viewSpectraState; const spectra = activeSpectra?.[activeTab]?.filter( (spectrum) => spectrum?.selected, ); diff --git a/src/component/reducer/helper/getActiveSpectraAsObject.ts b/src/component/reducer/helper/getActiveSpectraAsObject.ts index c83d60c97..f005ddd01 100644 --- a/src/component/reducer/helper/getActiveSpectraAsObject.ts +++ b/src/component/reducer/helper/getActiveSpectraAsObject.ts @@ -7,7 +7,10 @@ import { getActiveSpectra } from './getActiveSpectra.js'; export function getActiveSpectraAsObject( state: Draft | State, ): Record | null { - const activeSpectra = getActiveSpectra(state); + const { + view: { spectra }, + } = state; + const activeSpectra = getActiveSpectra(spectra); if (!activeSpectra || activeSpectra?.length === 0) return null; diff --git a/src/component/toolbar/ToolTypes.ts b/src/component/toolbar/ToolTypes.ts index d892d2e68..1335d41f8 100644 --- a/src/component/toolbar/ToolTypes.ts +++ b/src/component/toolbar/ToolTypes.ts @@ -10,13 +10,22 @@ export interface ToolOptionItem { id: Tool; label: string; mode?: DisplayerMode; - spectraOptions?: Array< - | { - info?: Array<{ key: InfoKey; value: any }>; // check if the active spectrum has these info - active: true; - } - | { active: false } - >; + spectraFilter?: Array<{ + info?: Array<{ key: InfoKey; value: any }>; // check if the active spectrum has these info + }>; + /** + * Minium and Maximum number of spectra that must be selected + * @default({min:1,max:1}}) exact one spectrum selected + */ + selectedSpectra?: { + min?: number; + max?: number; + }; /** + * controls how spectraFilter is evaluated + * 'all' : every selected spectrum must meet spectraFilter. + * 'any' : at least one selected spectrum must meet the spectraFilter. + */ + spectraMatch?: 'any' | 'all'; isToggle: boolean; hasOptionPanel: boolean; isFilter: boolean; @@ -48,15 +57,13 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, - }, - { - active: false, }, ], + selectedSpectra: { min: 1 }, + spectraMatch: 'all', isToggle: true, }, integral: { @@ -65,10 +72,9 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, }, ], isToggle: true, @@ -79,9 +85,9 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { - active: true, + info: [{ key: 'isFt', value: true }], }, ], isToggle: true, @@ -92,10 +98,9 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: false, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, }, ], isToggle: true, @@ -106,10 +111,9 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, }, ], isToggle: true, @@ -120,10 +124,9 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, }, ], isToggle: true, @@ -134,13 +137,12 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFid', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: true, @@ -151,13 +153,12 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFid', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: true, @@ -168,13 +169,12 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFid', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: true, @@ -185,14 +185,13 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFid', value: true }, { key: 'isFtDimensionOne', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: true, @@ -203,13 +202,12 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFt', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: true, @@ -220,10 +218,9 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, }, ], isToggle: true, @@ -234,15 +231,12 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, - }, - { - active: false, }, ], + selectedSpectra: { min: 0 }, isToggle: true, }, exclusionZones: { @@ -251,15 +245,12 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: true, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, - }, - { - active: false, }, ], + selectedSpectra: { min: 0 }, isToggle: true, }, matrixGenerationExclusionZones: { @@ -268,15 +259,12 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, - }, - { - active: false, }, ], + selectedSpectra: { min: 0 }, isToggle: true, }, databaseRangesSelection: { @@ -285,10 +273,9 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, }, ], isToggle: true, @@ -306,13 +293,12 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: true, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFid', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: false, @@ -323,13 +309,12 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: true, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFid', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: false, @@ -340,14 +325,13 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: true, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFtDimensionOne', value: true }, { key: 'isFt', value: false }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: false, @@ -358,10 +342,9 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, }, ], isToggle: true, @@ -379,10 +362,9 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isComplex', value: true }], - active: true, }, ], isToggle: false, @@ -401,15 +383,12 @@ export const options: RecordOptions = { hasOptionPanel: false, isFilter: false, mode: '1D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFt', value: true }], - active: true, - }, - { - active: false, }, ], + selectedSpectra: { min: 0 }, isToggle: false, }, zoomOut: { @@ -432,10 +411,9 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [{ key: 'isFid', value: true }], - active: true, }, ], isToggle: true, @@ -446,14 +424,13 @@ export const options: RecordOptions = { hasOptionPanel: true, isFilter: true, mode: '2D', - spectraOptions: [ + spectraFilter: [ { info: [ { key: 'isFid', value: true }, { key: 'isFtDimensionOne', value: true }, { key: 'isComplex', value: true }, ], - active: true, }, ], isToggle: true, diff --git a/src/component/toolbar/nmr_toolbar.tsx b/src/component/toolbar/nmr_toolbar.tsx index c53383d40..da56c4b9c 100644 --- a/src/component/toolbar/nmr_toolbar.tsx +++ b/src/component/toolbar/nmr_toolbar.tsx @@ -37,7 +37,6 @@ import { ToolbarPopoverItem } from '../elements/ToolbarPopoverItem.js'; import { useExportManagerAPI } from '../elements/export/ExportManager.js'; import { useActiveSpectrum } from '../hooks/useActiveSpectrum.js'; import useCheckExperimentalFeature from '../hooks/useCheckExperimentalFeature.js'; -import type { CheckOptions } from '../hooks/useCheckToolsVisibility.js'; import { useCheckToolsVisibility } from '../hooks/useCheckToolsVisibility.js'; import useDatumWithSpectraStatistics from '../hooks/useDatumWithSpectraStatistics.js'; import { useDialogToggle } from '../hooks/useDialogToggle.js'; @@ -58,7 +57,6 @@ import { EXPORT_MENU, IMPORT_MENU } from './toolbarMenu.js'; interface BaseToolItem extends Pick { id: MainTool; - checkOptions?: CheckOptions; isVisible?: boolean; } interface ToolItem extends BaseToolItem { @@ -303,7 +301,6 @@ export default function NMRToolbar() { 'Integrate multiple spectra at once and adjust integration zones by dragging their edges.', }, icon: , - checkOptions: { checkSpectrumType: false }, isVisible: ftCounter > 0, }, { @@ -398,7 +395,6 @@ export default function NMRToolbar() { description: `Define exclusion zones by clicking, dragging, releasing${!invert ? ' while holding SHIFT' : ''}. This option is practical for excluding large peaks like solvents.`, }, icon: , - checkOptions: { checkSpectrumType: false }, isVisible: ftCounter > 0, }, { @@ -510,10 +506,9 @@ export default function NMRToolbar() { {toolItems.map((item) => { - const { id, icon, tooltip, checkOptions, disabled, isVisible } = item; + const { id, icon, tooltip, disabled, isVisible } = item; const isToolVisible = - isButtonVisible(id, checkOptions) && - (isVisible === undefined || isVisible); + isButtonVisible(id) && (isVisible === undefined || isVisible); if (!isToolVisible) return null;