Skip to content

Commit edc4707

Browse files
Merge pull request #7760 from BloomBooks/BL15901_TopRightClickMenu
Move TopBar context menu to React (BL-15901) (#7760)
2 parents 91e6291 + 052b2ae commit edc4707

7 files changed

Lines changed: 288 additions & 163 deletions

File tree

src/BloomBrowserUI/react_components/TopBar/TopBar.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
kGreyOnDarkColor,
1515
} from "../../bloomMaterialUITheme";
1616
import { ScopedCssBaseline } from "@mui/material";
17+
import { TopBarContextMenu } from "./TopBarContextMenu";
1718

1819
export type WorkspaceTabId = "collection" | "edit" | "publish";
1920

@@ -71,6 +72,7 @@ export const TopBar: React.FunctionComponent = () => {
7172
"workspace",
7273
"tabs",
7374
);
75+
const topBarRef = React.useRef<HTMLDivElement>(null);
7476

7577
const tabStates = state.tabStates ?? defaultWorkspaceTabState.tabStates;
7678
const activeTab = React.useMemo((): WorkspaceTabId => {
@@ -110,6 +112,7 @@ export const TopBar: React.FunctionComponent = () => {
110112
CssBaseline would apply everywhere. */
111113
<ScopedCssBaseline>
112114
<div
115+
ref={topBarRef}
113116
css={css`
114117
background-color: ${getColorForTab(activeTab)};
115118
padding-top: 2px;
@@ -120,6 +123,7 @@ export const TopBar: React.FunctionComponent = () => {
120123
>
121124
<BloomTabs tabStates={tabStates} selectTab={handleSelectTab} />
122125
<TopBarControls activeTab={activeTab} />
126+
<TopBarContextMenu targetRef={topBarRef} />
123127
</div>
124128
</ScopedCssBaseline>
125129
);
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import * as React from "react";
2+
import { css } from "@emotion/react";
3+
import { getBoolean, post, postBoolean, postJson } from "../../utils/bloomApi";
4+
import Menu from "@mui/material/Menu";
5+
import Divider from "@mui/material/Divider";
6+
import {
7+
LocalizableMenuItem,
8+
LocalizableSelectableMenuItem,
9+
} from "../localizableMenuItem";
10+
import { useMountEffect } from "../../utils/useMountEffect";
11+
12+
interface ITopBarContextMenuPoint {
13+
clientX: number;
14+
clientY: number;
15+
}
16+
17+
interface ITopBarContextMenuItem {
18+
label: string;
19+
enabled?: boolean;
20+
selected?: boolean;
21+
onClick?: () => void;
22+
}
23+
24+
export const TopBarContextMenu: React.FunctionComponent<{
25+
targetRef: React.RefObject<HTMLDivElement>;
26+
}> = (props) => {
27+
const [menuPoint, setMenuPoint] = React.useState<ITopBarContextMenuPoint>();
28+
const [currentlyMeasuring, setCurrentlyMeasuring] = React.useState(false);
29+
const [alwaysMeasurePerformance, setAlwaysMeasurePerformance] =
30+
React.useState(false);
31+
const [isMeddlingWithNewFiles, setIsMeddlingWithNewFiles] =
32+
React.useState(false);
33+
34+
const onClose = React.useCallback(() => {
35+
setMenuPoint(undefined);
36+
}, []);
37+
38+
useMountEffect(() => {
39+
// CurrentlyMeasuring is only turned on from this menu, so local state can own it
40+
// once we establish the initial persisted setting. (No need to ask the back end.)
41+
getBoolean("app/alwaysMeasurePerformance", (value) => {
42+
setAlwaysMeasurePerformance(value);
43+
if (value) {
44+
setCurrentlyMeasuring(true);
45+
}
46+
});
47+
// Even though this is only controlled from this menu, meddling could be turned on
48+
// and then Bloom could restart or reload a collection. So we do need to ask
49+
// the back end for the current value.
50+
getBoolean("app/isMeddlingWithNewFiles", (value) => {
51+
setIsMeddlingWithNewFiles(value);
52+
});
53+
54+
const target = props.targetRef.current;
55+
if (!target) {
56+
return;
57+
}
58+
59+
const handleTopBarContextMenu = (event: MouseEvent) => {
60+
// Don't block the Ctrl+RightClick context menu
61+
if (event.ctrlKey) {
62+
return;
63+
}
64+
65+
event.preventDefault();
66+
event.stopPropagation();
67+
68+
setMenuPoint({
69+
clientX: event.clientX,
70+
clientY: event.clientY,
71+
});
72+
};
73+
74+
target.addEventListener("contextmenu", handleTopBarContextMenu);
75+
76+
return () => {
77+
target.removeEventListener("contextmenu", handleTopBarContextMenu);
78+
};
79+
});
80+
81+
function handleResizeWindow(width: number, height: number) {
82+
postJson("app/resizeWindow", {
83+
width,
84+
height,
85+
});
86+
}
87+
88+
const menuItems = React.useMemo<ITopBarContextMenuItem[]>(() => {
89+
return [
90+
{
91+
label: "1024 x 586 Low-end netbook with windows Task bar",
92+
onClick: () => {
93+
handleResizeWindow(1024, 586);
94+
},
95+
},
96+
{ label: "-" },
97+
{
98+
label: "800 x 600",
99+
onClick: () => {
100+
handleResizeWindow(800, 600);
101+
},
102+
},
103+
{
104+
label: "1024 x 600",
105+
onClick: () => {
106+
handleResizeWindow(1024, 600);
107+
},
108+
},
109+
{
110+
label: "1024 x 768",
111+
onClick: () => {
112+
handleResizeWindow(1024, 768);
113+
},
114+
},
115+
{ label: "-" },
116+
{
117+
label: "Always Measure Performance",
118+
selected: alwaysMeasurePerformance,
119+
onClick: () => {
120+
const newValue = !alwaysMeasurePerformance;
121+
postBoolean("app/alwaysMeasurePerformance", newValue);
122+
setAlwaysMeasurePerformance(newValue);
123+
if (newValue) {
124+
setCurrentlyMeasuring(true);
125+
}
126+
},
127+
},
128+
{
129+
label: currentlyMeasuring
130+
? "Currently Measuring Performance"
131+
: "Start Measuring Performance",
132+
enabled: !currentlyMeasuring,
133+
onClick: () => {
134+
post("app/startMeasuringPerformance");
135+
setCurrentlyMeasuring(true);
136+
},
137+
},
138+
{
139+
label: "Show Performance Page",
140+
enabled: currentlyMeasuring,
141+
onClick: () => {
142+
post("app/showPerformancePage");
143+
},
144+
},
145+
{
146+
label: isMeddlingWithNewFiles
147+
? "Stop Meddling with New Files"
148+
: "Meddle with New Files",
149+
onClick: () => {
150+
const newValue = !isMeddlingWithNewFiles;
151+
postBoolean("app/isMeddlingWithNewFiles", newValue);
152+
setIsMeddlingWithNewFiles(newValue);
153+
},
154+
},
155+
];
156+
}, [alwaysMeasurePerformance, currentlyMeasuring, isMeddlingWithNewFiles]);
157+
158+
return (
159+
<Menu
160+
keepMounted={true}
161+
open={!!menuPoint}
162+
onClose={onClose}
163+
anchorReference="anchorPosition"
164+
anchorPosition={
165+
menuPoint
166+
? {
167+
top: menuPoint.clientY,
168+
left: menuPoint.clientX,
169+
}
170+
: undefined
171+
}
172+
slotProps={{
173+
paper: {
174+
css: css`
175+
min-width: 220px;
176+
max-width: 440px;
177+
`,
178+
},
179+
}}
180+
>
181+
{menuItems.map((item, index) => {
182+
if (item.label === "-") {
183+
return <Divider key={`separator-${index}`} />;
184+
}
185+
186+
const commonProps = {
187+
key: `${item.label ?? index}`,
188+
english: item.label,
189+
l10nId: null,
190+
onClick: () => {
191+
item.onClick?.();
192+
onClose();
193+
},
194+
disabled: item.enabled === false,
195+
};
196+
197+
return item.selected !== undefined ? (
198+
<LocalizableSelectableMenuItem
199+
{...commonProps}
200+
selected={item.selected}
201+
/>
202+
) : (
203+
<LocalizableMenuItem
204+
{...commonProps}
205+
hasLeadingIconSpace={true}
206+
/>
207+
);
208+
})}
209+
</Menu>
210+
);
211+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { useEffect } from "react";
2+
3+
export function useMountEffect(effect: () => void | (() => void)) {
4+
// eslint-disable-next-line react-hooks/exhaustive-deps
5+
useEffect(effect, []);
6+
}

src/BloomExe/Shell.Designer.cs

Lines changed: 0 additions & 108 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)