diff --git a/src/fileContext.tsx b/src/fileContext.tsx index 410543fd..e0721129 100644 --- a/src/fileContext.tsx +++ b/src/fileContext.tsx @@ -7,6 +7,7 @@ * It is provided to widgets using a React context. */ import React from "react"; +import log from "loglevel"; import { useHistory, useLocation } from "react-router-dom"; import { MacroMap, macrosEqual } from "./types/macros"; @@ -280,3 +281,11 @@ export const FileProvider: React.FC = ( ); }; + +// Special context for exit buttons. +// A widget can register itself as handling exit actions and provide +// the function to do so via this context. +export type ExitContextType = () => void; +export const ExitFileContext = React.createContext(() => { + log.warn("Exit action has no consumer."); +}); diff --git a/src/ui/widgets/ActionButton/actionButton.tsx b/src/ui/widgets/ActionButton/actionButton.tsx index 073454a9..5d3f8b67 100644 --- a/src/ui/widgets/ActionButton/actionButton.tsx +++ b/src/ui/widgets/ActionButton/actionButton.tsx @@ -16,7 +16,7 @@ import { Color } from "../../../types/color"; import { Font } from "../../../types/font"; import { Border, BorderStyle } from "../../../types/border"; import { MacroContext } from "../../../types/macros"; -import { FileContext } from "../../../fileContext"; +import { ExitFileContext, FileContext } from "../../../fileContext"; export interface ActionButtonProps { text: string; @@ -89,10 +89,16 @@ export const ActionButtonWidget = ( ): JSX.Element => { // Function to send the value on to the PV const files = useContext(FileContext); + const exitContext = useContext(ExitFileContext); const parentMacros = useContext(MacroContext).macros; function onClick(event: React.MouseEvent): void { if (props.actions !== undefined) - executeActions(props.actions as WidgetActions, files, parentMacros); + executeActions( + props.actions as WidgetActions, + files, + exitContext, + parentMacros + ); } return ( - -
- fileContext.removePage(props.location)} + > +
+ +
+ > + +
-
+ ); } }; diff --git a/src/ui/widgets/EmbeddedDisplay/opiParser.ts b/src/ui/widgets/EmbeddedDisplay/opiParser.ts index ec468e6b..e4b6da3c 100644 --- a/src/ui/widgets/EmbeddedDisplay/opiParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/opiParser.ts @@ -26,7 +26,8 @@ import { WRITE_PV, OPEN_WEBPAGE, WidgetActions, - OPEN_TAB + OPEN_TAB, + EXIT } from "../widgetActions"; export interface XmlDescription { @@ -578,7 +579,36 @@ function opiPatchPaths( } } -export const OPI_PATCHERS: PatchFunction[] = [opiPatchRules, opiPatchPaths]; +function opiPatchActions(widgetDescription: WidgetDescription): void { + if ( + widgetDescription.type === "actionbutton" && + widgetDescription.text && + widgetDescription.text.toLowerCase() === "exit" + ) { + if ( + !widgetDescription.actions || + widgetDescription.actions.actions.length === 0 + ) { + widgetDescription.actions = { + executeAsOne: false, + actions: [ + { + type: EXIT, + exitInfo: { + description: "Exit" + } + } + ] + }; + } + } +} + +export const OPI_PATCHERS: PatchFunction[] = [ + opiPatchRules, + opiPatchPaths, + opiPatchActions +]; export function parseOpi( xmlString: string, diff --git a/src/ui/widgets/SimpleSymbol/simpleSymbol.tsx b/src/ui/widgets/SimpleSymbol/simpleSymbol.tsx index 1c5e900a..5e1477fb 100644 --- a/src/ui/widgets/SimpleSymbol/simpleSymbol.tsx +++ b/src/ui/widgets/SimpleSymbol/simpleSymbol.tsx @@ -16,7 +16,7 @@ import { import { registerWidget } from "../register"; import { executeActions, WidgetActions } from "../widgetActions"; import { MacroContext } from "../../../types/macros"; -import { FileContext } from "../../../fileContext"; +import { ExitFileContext, FileContext } from "../../../fileContext"; const SimpleSymbolProps = { imageFile: StringProp, @@ -45,10 +45,16 @@ export const SimpleSymbolComponent = ( props: SimpleSymbolComponentProps ): JSX.Element => { const files = useContext(FileContext); + const exitContext = useContext(ExitFileContext); const parentMacros = useContext(MacroContext).macros; function onClick(event: React.MouseEvent): void { if (props.actions !== undefined) { - executeActions(props.actions as WidgetActions, files, parentMacros); + executeActions( + props.actions as WidgetActions, + files, + exitContext, + parentMacros + ); } } // Render the imageIndex-th part of the larger png. diff --git a/src/ui/widgets/Symbol/symbol.tsx b/src/ui/widgets/Symbol/symbol.tsx index 4700d2c8..fb790c90 100644 --- a/src/ui/widgets/Symbol/symbol.tsx +++ b/src/ui/widgets/Symbol/symbol.tsx @@ -20,7 +20,7 @@ import { LabelComponent } from "../Label/label"; import { Color } from "../../../types/color"; import { executeActions, WidgetActions } from "../widgetActions"; import { MacroContext } from "../../../types/macros"; -import { FileContext } from "../../../fileContext"; +import { ExitFileContext, FileContext } from "../../../fileContext"; import { DType } from "../../../types/dtypes"; const SymbolProps = { @@ -101,10 +101,16 @@ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => { } const files = useContext(FileContext); + const exitContext = useContext(ExitFileContext); const parentMacros = useContext(MacroContext).macros; function onClick(event: React.MouseEvent): void { if (props.actions !== undefined) { - executeActions(props.actions as WidgetActions, files, parentMacros); + executeActions( + props.actions as WidgetActions, + files, + exitContext, + parentMacros + ); } } diff --git a/src/ui/widgets/Tabs/dynamicTabs.tsx b/src/ui/widgets/Tabs/dynamicTabs.tsx index f8fe6cae..6ca8d201 100644 --- a/src/ui/widgets/Tabs/dynamicTabs.tsx +++ b/src/ui/widgets/Tabs/dynamicTabs.tsx @@ -22,7 +22,7 @@ import { import { EmbeddedDisplay } from "../EmbeddedDisplay/embeddedDisplay"; import { RelativePosition } from "../../../types/position"; -import { FileContext } from "../../../fileContext"; +import { ExitFileContext, FileContext } from "../../../fileContext"; import { TabBar } from "./tabs"; export const DynamicTabsProps = { @@ -76,17 +76,23 @@ export const DynamicTabsComponent = ( const [tabName, fileDesc] = openTabs[index]; fileContext.removeTab(props.location, tabName, fileDesc); }; + const closeCurrentTab = (): void => { + const [tabName, fileDesc] = openTabs[selectedTab]; + fileContext.removeTab(props.location, tabName, fileDesc); + }; return ( -
- - {children[selectedTab]} -
+ closeCurrentTab()}> +
+ + {children[selectedTab]} +
+
); } }; diff --git a/src/ui/widgets/widget.tsx b/src/ui/widgets/widget.tsx index de3fbef0..a3da48ed 100644 --- a/src/ui/widgets/widget.tsx +++ b/src/ui/widgets/widget.tsx @@ -15,7 +15,7 @@ import { Color } from "../../types/color"; import { AlarmQuality } from "../../types/dtypes"; import { Font } from "../../types/font"; import { OutlineContext } from "../../outlineContext"; -import { FileContext } from "../../fileContext"; +import { ExitFileContext, FileContext } from "../../fileContext"; import { executeAction, WidgetAction, WidgetActions } from "./widgetActions"; import { Popover } from "react-tiny-popover"; import { resolveTooltip } from "./tooltip"; @@ -185,6 +185,7 @@ export const Widget = ( ): JSX.Element => { const [id] = useId(); const files = useContext(FileContext); + const exitContext = useContext(ExitFileContext); // Logic for context menu. const [contextOpen, setContextOpen] = useState(false); @@ -218,7 +219,7 @@ export const Widget = ( } function triggerCallback(action: WidgetAction): void { - executeAction(action, files); + executeAction(action, files, exitContext); setContextOpen(false); } let tooltip = props.tooltip; diff --git a/src/ui/widgets/widgetActions.ts b/src/ui/widgets/widgetActions.ts index afa3a0ac..e0d83aad 100644 --- a/src/ui/widgets/widgetActions.ts +++ b/src/ui/widgets/widgetActions.ts @@ -4,7 +4,7 @@ import log from "loglevel"; import { MacroMap } from "../../types/macros"; import { DType } from "../../types/dtypes"; import { DynamicContent } from "./propTypes"; -import { FileContextType } from "../../fileContext"; +import { ExitContextType, FileContextType } from "../../fileContext"; export const OPEN_PAGE = "OPEN_PAGE"; export const CLOSE_PAGE = "CLOSE_PAGE"; @@ -12,6 +12,7 @@ export const OPEN_TAB = "OPEN_TAB"; export const CLOSE_TAB = "CLOSE_TAB"; export const OPEN_WEBPAGE = "OPEN_WEBPAGE"; export const WRITE_PV = "WRITE_PV"; +export const EXIT = "EXIT"; /* Giving info properties to each of the following works around a difficulty with TypeScript and PropTypes, where there's a problem @@ -46,7 +47,14 @@ export interface WritePv { }; } -export type WidgetAction = OpenWebpage | WritePv | DynamicAction; +export interface Exit { + type: typeof EXIT; + exitInfo: { + description?: string; + }; +} + +export type WidgetAction = OpenWebpage | WritePv | DynamicAction | Exit; export interface WidgetActions { actions: WidgetAction[]; @@ -98,6 +106,12 @@ export const getActionDescription = (action: WidgetAction): string => { } else { return `Close tab ${action.dynamicInfo.name}`; } + case EXIT: + if (action.exitInfo.description) { + return action.exitInfo.description; + } else { + return `Close current screen`; + } default: throw new InvalidAction(action); } @@ -148,6 +162,7 @@ export const closeTab = ( export const executeAction = ( action: WidgetAction, files?: FileContextType, + exitContext?: ExitContextType, parentMacros?: MacroMap ): void => { switch (action.type) { @@ -191,6 +206,13 @@ export const executeAction = ( } writePv(action.writePvInfo.pvName, dtypeVal); break; + case EXIT: + if (exitContext) { + exitContext(); + } else { + log.error("Tried to exit but no exit context passed"); + } + break; default: throw new InvalidAction(action); } @@ -199,6 +221,7 @@ export const executeAction = ( export const executeActions = ( actions: WidgetActions, files?: FileContextType, + exitContext?: ExitContextType, parentMacros?: MacroMap ): void => { if (actions.actions.length > 0) { @@ -210,7 +233,7 @@ export const executeActions = ( } for (const action of toExecute) { log.info(`Executing an action: ${getActionDescription(action)}`); - executeAction(action, files, parentMacros); + executeAction(action, files, exitContext, parentMacros); } } };