Skip to content

Commit 5db9299

Browse files
833 add warning if connected to aircraft and try to quit fgcs (#840)
* Moved close action to function in Toolbar component * Imported connectedToDrone selector * Added confirm exit modal * Added application slice and relevant state for controlling modal * Connected modal to slice - appears working * Lint * Kush's requests added. * resolve formatting error * add warning for mac with native dialog box * addressing lint errors --------- Co-authored-by: Kwashie A. <104215256+Kwash67@users.noreply.github.com>
1 parent ac789fe commit 5db9299

7 files changed

Lines changed: 168 additions & 11 deletions

File tree

gcs/electron/main.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ if (process.platform === "linux") {
5757

5858
let win: BrowserWindow | null
5959
let loadingWin: BrowserWindow | null
60+
let isConnectedToDrone = false
61+
let quittingApproved = false
6062

6163
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
6264
const VITE_DEV_SERVER_URL = process.env["VITE_DEV_SERVER_URL"]
@@ -138,6 +140,11 @@ ipcMain.handle("settings:save-settings", (_, settings) => {
138140
saveUserConfiguration(settings)
139141
})
140142

143+
// Cache connection state from renderer
144+
ipcMain.on("app:connected-state", (_event, connected: boolean) => {
145+
isConnectedToDrone = Boolean(connected)
146+
})
147+
141148
ipcMain.handle("app:is-mac", () => {
142149
return process.platform == "darwin"
143150
})
@@ -431,12 +438,46 @@ app.on("window-all-closed", () => {
431438

432439
// To ensure that the backend process is killed with Cmd + Q on macOS,
433440
// listen to the before-quit event.
434-
app.on("before-quit", () => {
435-
if (process.platform === "darwin" && pythonBackend) {
436-
console.log("Stopping backend")
437-
spawnSync("pkill", ["-f", "fgcs_backend"])
438-
pythonBackend = null
439-
closeWindows()
441+
app.on("before-quit", (e) => {
442+
if (process.platform !== "darwin") return
443+
444+
// User already approved, let it proceed without re-prompting
445+
if (quittingApproved) return
446+
447+
if (isConnectedToDrone && win && !win.isDestroyed()) {
448+
e.preventDefault()
449+
const choice = dialog.showMessageBoxSync(win, {
450+
type: "warning",
451+
buttons: ["Cancel", "Quit"],
452+
defaultId: 0,
453+
title: "Confirm Quit",
454+
message: "Are you sure you want to quit FGCS?",
455+
detail: "You are connected to an aircraft.",
456+
})
457+
if (choice === 1) {
458+
quittingApproved = true
459+
if (pythonBackend) {
460+
console.log("Stopping backend")
461+
spawnSync("pkill", ["-f", "fgcs_backend"])
462+
pythonBackend = null
463+
}
464+
// Close all popout windows
465+
closeWindows()
466+
// Destroy main window
467+
if (win && !win.isDestroyed()) {
468+
win.destroy()
469+
}
470+
app.quit()
471+
}
472+
// choice === 0 (Cancel): do nothing, quit is prevented
473+
} else {
474+
// Not connected or no window: stop backend and proceed
475+
if (pythonBackend) {
476+
console.log("Stopping backend")
477+
spawnSync("pkill", ["-f", "fgcs_backend"])
478+
pythonBackend = null
479+
closeWindows()
480+
}
440481
}
441482
})
442483

gcs/electron/preload.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ const ALLOWED_SEND_CHANNELS = [
4747
"window:zoom-in",
4848
"window:zoom-out",
4949
"window:open-file-in-explorer",
50+
// app state updates
51+
"app:connected-state",
5052
]
5153

5254
const ALLOWED_ON_CHANNELS = [

gcs/src/components/mainContent.jsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,30 @@ import Navbar from "./navbar"
2525

2626
// Redux
2727
import { ErrorBoundary } from "react-error-boundary"
28-
import { useDispatch } from "react-redux"
28+
import { useDispatch, useSelector } from "react-redux"
2929
import { initSocket } from "../redux/slices/socketSlice"
30+
import { selectConnectedToDrone } from "../redux/slices/droneConnectionSlice"
3031
import AlertProvider from "./dashboard/alerts/alertProvider"
3132
import ErrorBoundaryFallback from "./error/errorBoundary"
3233

3334
export default function AppContent() {
3435
// Setup sockets for redux
3536
const dispatch = useDispatch()
37+
const connectedToDrone = useSelector(selectConnectedToDrone)
3638
useEffect(() => {
3739
dispatch(initSocket())
3840
}, [])
3941

42+
// Send connection state changes to main so it can own quit policy on macOS
43+
useEffect(() => {
44+
try {
45+
window.ipcRenderer.send("app:connected-state", connectedToDrone)
46+
} catch {
47+
// Ignore IPC errors if main process isn't ready
48+
console.log("IPC Call Failed: app:connected-state")
49+
}
50+
}, [connectedToDrone])
51+
4052
return (
4153
<SettingsProvider>
4254
<SingleRunWrapper>
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Custom Imports
2+
import { Group, Modal, Button, Text } from "@mantine/core"
3+
4+
// Redux
5+
import { useDispatch, useSelector } from "react-redux"
6+
import {
7+
selectConfirmExitModalOpen,
8+
setConfirmExitModalOpen,
9+
} from "../../redux/slices/applicationSlice"
10+
11+
// Tailwind
12+
import resolveConfig from "tailwindcss/resolveConfig"
13+
import tailwindConfig from "../../../tailwind.config"
14+
const tailwindColors = resolveConfig(tailwindConfig).theme.colors
15+
16+
export default function ConfirmExitModal() {
17+
const dispatch = useDispatch()
18+
const modalOpen = useSelector(selectConfirmExitModalOpen)
19+
20+
const confirmExit = () => {
21+
window.ipcRenderer.send("window:close")
22+
}
23+
24+
return (
25+
<Modal
26+
opened={modalOpen}
27+
onClose={() => dispatch(setConfirmExitModalOpen(false))}
28+
title="Are you sure you want to quit FGCS?"
29+
centered
30+
overlayProps={{
31+
backgroundOpacity: 0.55,
32+
blur: 3,
33+
}}
34+
styles={{
35+
content: {
36+
borderRadius: "0.5rem",
37+
},
38+
}}
39+
withCloseButton={false}
40+
>
41+
<Text mb={16} c="dimmed" size="sm">
42+
You are connected to an aircraft, are you sure you want to quit FGCS?
43+
</Text>
44+
<Group justify="space-between" className="pt-4">
45+
<Button
46+
variant="filled"
47+
onClick={() => dispatch(setConfirmExitModalOpen(false))}
48+
>
49+
Cancel
50+
</Button>
51+
<Button
52+
variant="filled"
53+
type="submit"
54+
color={tailwindColors.red[600]}
55+
onClick={() => confirmExit()}
56+
>
57+
Quit
58+
</Button>
59+
</Group>
60+
</Modal>
61+
)
62+
}

gcs/src/components/toolbar/toolbar.jsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,34 @@ import { CloseIcon, MaximizeIcon, MinimizeIcon } from "./icons.jsx"
1414
import AdvancedMenu from "./menus/advanced.jsx"
1515
import FileMenu from "./menus/file.jsx"
1616
import ViewMenu from "./menus/view.jsx"
17+
import ConfirmExitModal from "./confirmExitModal.jsx"
18+
19+
// Redux
20+
import { useDispatch, useSelector } from "react-redux"
21+
import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice.js"
22+
import { setConfirmExitModalOpen } from "../../redux/slices/applicationSlice.js"
1723

1824
export default function Toolbar() {
25+
const dispatch = useDispatch()
1926
const [areMenusActive, setMenusActive] = useState(false)
2027
const [isMac, setIsMac] = useState(false)
2128

29+
const connectedToDrone = useSelector(selectConnectedToDrone)
30+
2231
useEffect(() => {
2332
window.ipcRenderer.invoke("app:is-mac").then((result) => {
2433
setIsMac(result)
2534
})
2635
}, [])
2736

37+
const onClose = () => {
38+
if (connectedToDrone) {
39+
dispatch(setConfirmExitModalOpen(true))
40+
} else {
41+
window.ipcRenderer.send("window:close", [])
42+
}
43+
}
44+
2845
return (
2946
<>
3047
<div
@@ -100,16 +117,16 @@ export default function Toolbar() {
100117
<div
101118
title="Close"
102119
className="px-3 flex items-center h-full no-drag cursor-pointer group hover:bg-red-500"
103-
onClick={() => {
104-
window.ipcRenderer.send("window:close", [])
105-
}}
120+
onClick={() => onClose()}
106121
label="Close"
107122
>
108123
<CloseIcon className="stroke-slate-400 group-hover:stroke-white" />
109124
</div>
110125
</div>
111126
)}
112127
</div>
128+
129+
<ConfirmExitModal></ConfirmExitModal>
113130
</>
114131
)
115132
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { createSlice } from "@reduxjs/toolkit"
2+
3+
const applicationSlice = createSlice({
4+
name: "application",
5+
initialState: { confirmExitModalOpen: false },
6+
reducers: {
7+
// Setters
8+
setConfirmExitModalOpen: (state, action) => {
9+
if (action.payload !== state.confirmExitModalOpen) {
10+
state.confirmExitModalOpen = action.payload
11+
}
12+
},
13+
},
14+
selectors: {
15+
selectConfirmExitModalOpen: (state) => state.confirmExitModalOpen,
16+
},
17+
})
18+
19+
export const { setConfirmExitModalOpen } = applicationSlice.actions
20+
export const { selectConfirmExitModalOpen } = applicationSlice.selectors
21+
22+
export default applicationSlice

gcs/src/redux/store.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { combineSlices, configureStore } from "@reduxjs/toolkit"
22
import droneInfoSlice, { setGraphValues } from "./slices/droneInfoSlice"
33
import logAnalyserSlice from "./slices/logAnalyserSlice"
44
import socketSlice from "./slices/socketSlice"
5-
5+
import applicationSlice from "./slices/applicationSlice"
66
import socketMiddleware from "./middleware/socketMiddleware"
77
import configSlice from "./slices/configSlice"
88
import droneConnectionSlice, {
@@ -30,6 +30,7 @@ const rootReducer = combineSlices(
3030
statusTextSlice,
3131
paramsSlice,
3232
configSlice,
33+
applicationSlice,
3334
)
3435

3536
export const store = configureStore({

0 commit comments

Comments
 (0)