Skip to content
Merged
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
53 changes: 47 additions & 6 deletions gcs/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ if (process.platform === "linux") {

let win: BrowserWindow | null
let loadingWin: BrowserWindow | null
let isConnectedToDrone = false
let quittingApproved = false

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

// Cache connection state from renderer
ipcMain.on("app:connected-state", (_event, connected: boolean) => {
isConnectedToDrone = Boolean(connected)
})

ipcMain.handle("app:is-mac", () => {
return process.platform == "darwin"
})
Expand Down Expand Up @@ -431,12 +438,46 @@ app.on("window-all-closed", () => {

// To ensure that the backend process is killed with Cmd + Q on macOS,
// listen to the before-quit event.
app.on("before-quit", () => {
if (process.platform === "darwin" && pythonBackend) {
console.log("Stopping backend")
spawnSync("pkill", ["-f", "fgcs_backend"])
pythonBackend = null
closeWindows()
app.on("before-quit", (e) => {
if (process.platform !== "darwin") return

// User already approved, let it proceed without re-prompting
if (quittingApproved) return

if (isConnectedToDrone && win && !win.isDestroyed()) {
e.preventDefault()
const choice = dialog.showMessageBoxSync(win, {
type: "warning",
buttons: ["Cancel", "Quit"],
defaultId: 0,
title: "Confirm Quit",
message: "Are you sure you want to quit FGCS?",
detail: "You are connected to an aircraft.",
})
if (choice === 1) {
quittingApproved = true
if (pythonBackend) {
console.log("Stopping backend")
spawnSync("pkill", ["-f", "fgcs_backend"])
pythonBackend = null
}
// Close all popout windows
closeWindows()
// Destroy main window
if (win && !win.isDestroyed()) {
win.destroy()
}
app.quit()
}
// choice === 0 (Cancel): do nothing, quit is prevented
} else {
// Not connected or no window: stop backend and proceed
if (pythonBackend) {
console.log("Stopping backend")
spawnSync("pkill", ["-f", "fgcs_backend"])
pythonBackend = null
closeWindows()
}
}
})

Expand Down
2 changes: 2 additions & 0 deletions gcs/electron/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const ALLOWED_SEND_CHANNELS = [
"window:zoom-in",
"window:zoom-out",
"window:open-file-in-explorer",
// app state updates
"app:connected-state",
]

const ALLOWED_ON_CHANNELS = [
Expand Down
14 changes: 13 additions & 1 deletion gcs/src/components/mainContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,30 @@ import Navbar from "./navbar"

// Redux
import { ErrorBoundary } from "react-error-boundary"
import { useDispatch } from "react-redux"
import { useDispatch, useSelector } from "react-redux"
import { initSocket } from "../redux/slices/socketSlice"
import { selectConnectedToDrone } from "../redux/slices/droneConnectionSlice"
import AlertProvider from "./dashboard/alerts/alertProvider"
import ErrorBoundaryFallback from "./error/errorBoundary"

export default function AppContent() {
// Setup sockets for redux
const dispatch = useDispatch()
const connectedToDrone = useSelector(selectConnectedToDrone)
useEffect(() => {
dispatch(initSocket())
}, [])

// Send connection state changes to main so it can own quit policy on macOS
useEffect(() => {
try {
window.ipcRenderer.send("app:connected-state", connectedToDrone)
} catch {
// Ignore IPC errors if main process isn't ready
console.log("IPC Call Failed: app:connected-state")
}
}, [connectedToDrone])

return (
<SettingsProvider>
<SingleRunWrapper>
Expand Down
62 changes: 62 additions & 0 deletions gcs/src/components/toolbar/confirmExitModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Custom Imports
import { Group, Modal, Button, Text } from "@mantine/core"

// Redux
import { useDispatch, useSelector } from "react-redux"
import {
selectConfirmExitModalOpen,
setConfirmExitModalOpen,
} from "../../redux/slices/applicationSlice"

// Tailwind
import resolveConfig from "tailwindcss/resolveConfig"
import tailwindConfig from "../../../tailwind.config"
const tailwindColors = resolveConfig(tailwindConfig).theme.colors

export default function ConfirmExitModal() {
const dispatch = useDispatch()
const modalOpen = useSelector(selectConfirmExitModalOpen)

const confirmExit = () => {
window.ipcRenderer.send("window:close")
}

return (
<Modal
opened={modalOpen}
onClose={() => dispatch(setConfirmExitModalOpen(false))}
title="Are you sure you want to quit FGCS?"
centered
overlayProps={{
backgroundOpacity: 0.55,
blur: 3,
}}
styles={{
content: {
borderRadius: "0.5rem",
},
}}
withCloseButton={false}
>
<Text mb={16} c="dimmed" size="sm">
You are connected to an aircraft, are you sure you want to quit FGCS?
</Text>
<Group justify="space-between" className="pt-4">
<Button
variant="filled"
onClick={() => dispatch(setConfirmExitModalOpen(false))}
>
Cancel
</Button>
<Button
variant="filled"
type="submit"
color={tailwindColors.red[600]}
onClick={() => confirmExit()}
>
Quit
</Button>
</Group>
</Modal>
)
}
23 changes: 20 additions & 3 deletions gcs/src/components/toolbar/toolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,34 @@ import { CloseIcon, MaximizeIcon, MinimizeIcon } from "./icons.jsx"
import AdvancedMenu from "./menus/advanced.jsx"
import FileMenu from "./menus/file.jsx"
import ViewMenu from "./menus/view.jsx"
import ConfirmExitModal from "./confirmExitModal.jsx"

// Redux
import { useDispatch, useSelector } from "react-redux"
import { selectConnectedToDrone } from "../../redux/slices/droneConnectionSlice.js"
import { setConfirmExitModalOpen } from "../../redux/slices/applicationSlice.js"

export default function Toolbar() {
const dispatch = useDispatch()
const [areMenusActive, setMenusActive] = useState(false)
const [isMac, setIsMac] = useState(false)

const connectedToDrone = useSelector(selectConnectedToDrone)

useEffect(() => {
window.ipcRenderer.invoke("app:is-mac").then((result) => {
setIsMac(result)
})
}, [])

const onClose = () => {
if (connectedToDrone) {
dispatch(setConfirmExitModalOpen(true))
} else {
window.ipcRenderer.send("window:close", [])
}
}

return (
<>
<div
Expand Down Expand Up @@ -100,16 +117,16 @@ export default function Toolbar() {
<div
title="Close"
className="px-3 flex items-center h-full no-drag cursor-pointer group hover:bg-red-500"
onClick={() => {
window.ipcRenderer.send("window:close", [])
}}
onClick={() => onClose()}
label="Close"
>
<CloseIcon className="stroke-slate-400 group-hover:stroke-white" />
</div>
</div>
)}
</div>

<ConfirmExitModal></ConfirmExitModal>
</>
)
}
22 changes: 22 additions & 0 deletions gcs/src/redux/slices/applicationSlice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createSlice } from "@reduxjs/toolkit"

const applicationSlice = createSlice({
name: "application",
initialState: { confirmExitModalOpen: false },
reducers: {
// Setters
setConfirmExitModalOpen: (state, action) => {
if (action.payload !== state.confirmExitModalOpen) {
state.confirmExitModalOpen = action.payload
}
},
},
selectors: {
selectConfirmExitModalOpen: (state) => state.confirmExitModalOpen,
},
})

export const { setConfirmExitModalOpen } = applicationSlice.actions
export const { selectConfirmExitModalOpen } = applicationSlice.selectors

export default applicationSlice
3 changes: 2 additions & 1 deletion gcs/src/redux/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { combineSlices, configureStore } from "@reduxjs/toolkit"
import droneInfoSlice, { setGraphValues } from "./slices/droneInfoSlice"
import logAnalyserSlice from "./slices/logAnalyserSlice"
import socketSlice from "./slices/socketSlice"

import applicationSlice from "./slices/applicationSlice"
import socketMiddleware from "./middleware/socketMiddleware"
import configSlice from "./slices/configSlice"
import droneConnectionSlice, {
Expand Down Expand Up @@ -30,6 +30,7 @@ const rootReducer = combineSlices(
statusTextSlice,
paramsSlice,
configSlice,
applicationSlice,
)

export const store = configureStore({
Expand Down