コード例 #1
0
    def __init__(self):
        super(QLiberationWindow, self).__init__()

        self.game: Optional[Game] = None
        self.game_model = GameModel()
        Dialog.set_game(self.game_model)
        self.ato_panel = QAirTaskingOrderPanel(self.game_model)
        self.info_panel = QInfoPanel(self.game)
        self.liberation_map = QLiberationMap(self.game_model)

        self.setGeometry(300, 100, 270, 100)
        self.setWindowTitle(f"DCS Liberation - v{VERSION}")
        self.setWindowIcon(QIcon("./resources/icon.png"))
        self.statusBar().showMessage('Ready')

        self.initUi()
        self.initActions()
        self.initMenuBar()
        self.initToolbar()
        self.connectSignals()

        screen = QDesktopWidget().screenGeometry()
        self.setGeometry(0, 0, screen.width(), screen.height())
        self.setWindowState(Qt.WindowMaximized)

        self.onGameGenerated(persistency.restore_game())
コード例 #2
0
    def __init__(self, game: Optional[Game]) -> None:
        super(QLiberationWindow, self).__init__()

        self.game = game
        self.game_model = GameModel(game)
        Dialog.set_game(self.game_model)
        self.ato_panel = QAirTaskingOrderPanel(self.game_model)
        self.info_panel = QInfoPanel(self.game)
        self.liberation_map = QLiberationMap(self.game_model, self)

        self.setGeometry(300, 100, 270, 100)
        self.updateWindowTitle()
        self.setWindowIcon(QIcon("./resources/icon.png"))
        self.statusBar().showMessage("Ready")

        self.initUi()
        self.initActions()
        self.initToolbar()
        self.initMenuBar()
        self.connectSignals()

        screen = QDesktopWidget().screenGeometry()
        self.setGeometry(0, 0, screen.width(), screen.height())
        self.setWindowState(Qt.WindowMaximized)

        if self.game is None:
            last_save_file = liberation_install.get_last_save_file()
            if last_save_file:
                try:
                    logging.info("Loading last saved game : " +
                                 str(last_save_file))
                    game = persistency.load_game(last_save_file)
                    self.onGameGenerated(game)
                    self.updateWindowTitle(last_save_file if game else None)
                except:
                    logging.info("Error loading latest save game")
            else:
                logging.info("No existing save game")
        else:
            self.onGameGenerated(self.game)
コード例 #3
0
    def __init__(self, game: Game | None, dev: bool) -> None:
        super().__init__()

        self._uncaught_exception_handler = UncaughtExceptionHandler(self)

        self.game = game
        self.sim_controller = SimController(self.game)
        self.sim_controller.sim_update.connect(EventStream.put_nowait)
        self.game_model = GameModel(game, self.sim_controller)
        GameContext.set_model(self.game_model)
        self.new_package_signal.connect(
            lambda target: Dialog.open_new_package_dialog(target, self))
        self.tgo_info_signal.connect(self.open_tgo_info_dialog)
        self.control_point_info_signal.connect(
            self.open_control_point_info_dialog)
        QtContext.set_callbacks(
            QtCallbacks(
                lambda target: self.new_package_signal.emit(target),
                lambda tgo: self.tgo_info_signal.emit(tgo),
                lambda cp: self.control_point_info_signal.emit(cp),
            ))
        Dialog.set_game(self.game_model)
        self.ato_panel = QAirTaskingOrderPanel(self.game_model)
        self.info_panel = QInfoPanel(self.game)
        self.liberation_map = QLiberationMap(self.game_model, dev, self)

        self.setGeometry(300, 100, 270, 100)
        self.updateWindowTitle()
        self.setWindowIcon(QIcon("./resources/icon.png"))
        self.statusBar().showMessage("Ready")

        self.initUi()
        self.initActions()
        self.initToolbar()
        self.initMenuBar()
        self.connectSignals()

        # Default to maximized on the main display if we don't have any persistent
        # configuration.
        screen = QDesktopWidget().screenGeometry()
        self.setGeometry(0, 0, screen.width(), screen.height())
        self.setWindowState(Qt.WindowMaximized)

        # But override it with the saved configuration if it exists.
        self._restore_window_geometry()

        if self.game is None:
            last_save_file = liberation_install.get_last_save_file()
            if last_save_file:
                try:
                    logging.info("Loading last saved game : " +
                                 str(last_save_file))
                    game = persistency.load_game(last_save_file)
                    self.onGameGenerated(game)
                    self.updateWindowTitle(last_save_file if game else None)
                except:
                    logging.info("Error loading latest save game")
            else:
                logging.info("No existing save game")
        else:
            self.onGameGenerated(self.game)
コード例 #4
0
class QLiberationWindow(QMainWindow):
    new_package_signal = Signal(MissionTarget)
    tgo_info_signal = Signal(TheaterGroundObject)
    control_point_info_signal = Signal(ControlPoint)

    def __init__(self, game: Game | None, dev: bool) -> None:
        super().__init__()

        self._uncaught_exception_handler = UncaughtExceptionHandler(self)

        self.game = game
        self.sim_controller = SimController(self.game)
        self.sim_controller.sim_update.connect(EventStream.put_nowait)
        self.game_model = GameModel(game, self.sim_controller)
        GameContext.set_model(self.game_model)
        self.new_package_signal.connect(
            lambda target: Dialog.open_new_package_dialog(target, self))
        self.tgo_info_signal.connect(self.open_tgo_info_dialog)
        self.control_point_info_signal.connect(
            self.open_control_point_info_dialog)
        QtContext.set_callbacks(
            QtCallbacks(
                lambda target: self.new_package_signal.emit(target),
                lambda tgo: self.tgo_info_signal.emit(tgo),
                lambda cp: self.control_point_info_signal.emit(cp),
            ))
        Dialog.set_game(self.game_model)
        self.ato_panel = QAirTaskingOrderPanel(self.game_model)
        self.info_panel = QInfoPanel(self.game)
        self.liberation_map = QLiberationMap(self.game_model, dev, self)

        self.setGeometry(300, 100, 270, 100)
        self.updateWindowTitle()
        self.setWindowIcon(QIcon("./resources/icon.png"))
        self.statusBar().showMessage("Ready")

        self.initUi()
        self.initActions()
        self.initToolbar()
        self.initMenuBar()
        self.connectSignals()

        # Default to maximized on the main display if we don't have any persistent
        # configuration.
        screen = QDesktopWidget().screenGeometry()
        self.setGeometry(0, 0, screen.width(), screen.height())
        self.setWindowState(Qt.WindowMaximized)

        # But override it with the saved configuration if it exists.
        self._restore_window_geometry()

        if self.game is None:
            last_save_file = liberation_install.get_last_save_file()
            if last_save_file:
                try:
                    logging.info("Loading last saved game : " +
                                 str(last_save_file))
                    game = persistency.load_game(last_save_file)
                    self.onGameGenerated(game)
                    self.updateWindowTitle(last_save_file if game else None)
                except:
                    logging.info("Error loading latest save game")
            else:
                logging.info("No existing save game")
        else:
            self.onGameGenerated(self.game)

    def initUi(self):
        hbox = QSplitter(Qt.Horizontal)
        vbox = QSplitter(Qt.Vertical)
        hbox.addWidget(self.ato_panel)
        hbox.addWidget(vbox)
        vbox.addWidget(self.liberation_map)
        vbox.addWidget(self.info_panel)

        # Will make the ATO sidebar as small as necessary to fit the content. In
        # practice this means it is sized by the hints in the panel.
        hbox.setSizes([1, 10000000])
        vbox.setSizes([600, 100])

        vbox = QVBoxLayout()
        vbox.setMargin(0)
        vbox.addWidget(QTopPanel(self.game_model, self.sim_controller))
        vbox.addWidget(hbox)

        central_widget = QWidget()
        central_widget.setLayout(vbox)
        self.setCentralWidget(central_widget)

    def connectSignals(self):
        GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
        GameUpdateSignal.get_instance().debriefingReceived.connect(
            self.onDebriefing)

    def initActions(self):
        self.newGameAction = QAction("&New Game", self)
        self.newGameAction.setIcon(QIcon(CONST.ICONS["New"]))
        self.newGameAction.triggered.connect(self.newGame)
        self.newGameAction.setShortcut("CTRL+N")

        self.openAction = QAction("&Open", self)
        self.openAction.setIcon(QIcon(CONST.ICONS["Open"]))
        self.openAction.triggered.connect(self.openFile)
        self.openAction.setShortcut("CTRL+O")

        self.saveGameAction = QAction("&Save", self)
        self.saveGameAction.setIcon(QIcon(CONST.ICONS["Save"]))
        self.saveGameAction.triggered.connect(self.saveGame)
        self.saveGameAction.setShortcut("CTRL+S")

        self.saveAsAction = QAction("Save &As", self)
        self.saveAsAction.setIcon(QIcon(CONST.ICONS["Save"]))
        self.saveAsAction.triggered.connect(self.saveGameAs)
        self.saveAsAction.setShortcut("CTRL+A")

        self.showAboutDialogAction = QAction("&About DCS Liberation", self)
        self.showAboutDialogAction.setIcon(QIcon.fromTheme("help-about"))
        self.showAboutDialogAction.triggered.connect(self.showAboutDialog)

        self.showLiberationPrefDialogAction = QAction("&Preferences", self)
        self.showLiberationPrefDialogAction.setIcon(
            QIcon.fromTheme("help-about"))
        self.showLiberationPrefDialogAction.triggered.connect(
            self.showLiberationDialog)

        self.openDiscordAction = QAction("&Discord Server", self)
        self.openDiscordAction.setIcon(CONST.ICONS["Discord"])
        self.openDiscordAction.triggered.connect(
            lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" +
                                            "bKrt" + "rkJ"))

        self.openGithubAction = QAction("&Github Repo", self)
        self.openGithubAction.setIcon(CONST.ICONS["Github"])
        self.openGithubAction.triggered.connect(
            lambda: webbrowser.open_new_tab(
                "https://github.com/dcs-liberation/dcs_liberation"))

        self.ukraineAction = QAction("&Ukraine", self)
        self.ukraineAction.setIcon(CONST.ICONS["Ukraine"])
        self.ukraineAction.triggered.connect(lambda: webbrowser.open_new_tab(
            "https://shdwp.github.io/ukraine/"))

        self.openLogsAction = QAction("Show &logs", self)
        self.openLogsAction.triggered.connect(self.showLogsDialog)

        self.openSettingsAction = QAction("Settings", self)
        self.openSettingsAction.setIcon(CONST.ICONS["Settings"])
        self.openSettingsAction.triggered.connect(self.showSettingsDialog)

        self.openStatsAction = QAction("Stats", self)
        self.openStatsAction.setIcon(CONST.ICONS["Statistics"])
        self.openStatsAction.triggered.connect(self.showStatsDialog)

        self.openNotesAction = QAction("Notes", self)
        self.openNotesAction.setIcon(CONST.ICONS["Notes"])
        self.openNotesAction.triggered.connect(self.showNotesDialog)

        self.importTemplatesAction = QAction("Import Layouts", self)
        self.importTemplatesAction.triggered.connect(self.import_templates)

        self.enable_game_actions(False)

    def enable_game_actions(self, enabled: bool):
        self.openSettingsAction.setVisible(enabled)
        self.openStatsAction.setVisible(enabled)
        self.openNotesAction.setVisible(enabled)

        # Also Disable SaveAction to prevent Keyboard Shortcut
        self.saveGameAction.setEnabled(enabled)
        self.saveGameAction.setVisible(enabled)
        self.saveAsAction.setEnabled(enabled)
        self.saveAsAction.setVisible(enabled)

    def initToolbar(self):
        self.tool_bar = self.addToolBar("File")
        self.tool_bar.addAction(self.newGameAction)
        self.tool_bar.addAction(self.openAction)
        self.tool_bar.addAction(self.saveGameAction)

        self.links_bar = self.addToolBar("Links")
        self.links_bar.addAction(self.openDiscordAction)
        self.links_bar.addAction(self.openGithubAction)
        self.links_bar.addAction(self.ukraineAction)

        self.actions_bar = self.addToolBar("Actions")
        self.actions_bar.addAction(self.openSettingsAction)
        self.actions_bar.addAction(self.openStatsAction)
        self.actions_bar.addAction(self.openNotesAction)

    def initMenuBar(self):
        self.menu = self.menuBar()

        file_menu = self.menu.addMenu("&File")
        file_menu.addAction(self.newGameAction)
        file_menu.addAction(self.openAction)
        file_menu.addSeparator()
        file_menu.addAction(self.saveGameAction)
        file_menu.addAction(self.saveAsAction)
        file_menu.addSeparator()
        file_menu.addAction(self.showLiberationPrefDialogAction)
        file_menu.addSeparator()
        file_menu.addAction("E&xit", self.close)

        tools_menu = self.menu.addMenu("&Developer tools")
        tools_menu.addAction(self.importTemplatesAction)

        help_menu = self.menu.addMenu("&Help")
        help_menu.addAction(self.openDiscordAction)
        help_menu.addAction(self.openGithubAction)
        help_menu.addAction(self.ukraineAction)
        help_menu.addAction(
            "&Releases",
            lambda: webbrowser.open_new_tab(
                "https://github.com/dcs-liberation/dcs_liberation/releases"),
        )
        help_menu.addAction("&Online Manual",
                            lambda: webbrowser.open_new_tab(URLS["Manual"]))
        help_menu.addAction(
            "&ED Forum Thread",
            lambda: webbrowser.open_new_tab(URLS["ForumThread"]))
        help_menu.addAction("Report an &issue",
                            lambda: webbrowser.open_new_tab(URLS["Issues"]))
        help_menu.addAction(self.openLogsAction)

        help_menu.addSeparator()
        help_menu.addAction(self.showAboutDialogAction)

    @staticmethod
    def make_display_rule_action(display_rule,
                                 group: Optional[QActionGroup] = None
                                 ) -> QAction:
        def make_check_closure():
            def closure():
                display_rule.value = action.isChecked()

            return closure

        action = QAction(f"&{display_rule.menu_text}", group)

        if display_rule.menu_text in CONST.ICONS.keys():
            action.setIcon(CONST.ICONS[display_rule.menu_text])

        action.setCheckable(True)
        action.setChecked(display_rule.value)
        action.toggled.connect(make_check_closure())
        return action

    def newGame(self):
        wizard = NewGameWizard(self)
        wizard.show()
        wizard.accepted.connect(
            lambda: self.onGameGenerated(wizard.generatedGame))

    def openFile(self):
        if self.game is not None and self.game.savepath:
            save_dir = self.game.savepath
        else:
            save_dir = str(persistency.save_dir())
        file = QFileDialog.getOpenFileName(
            self,
            "Select game file to open",
            dir=save_dir,
            filter="*.liberation",
        )
        if file is not None and file[0] != "":
            game = persistency.load_game(file[0])
            GameUpdateSignal.get_instance().game_loaded.emit(game)

            self.updateWindowTitle(file[0])

    def saveGame(self):
        logging.info("Saving game")

        if self.game.savepath:
            persistency.save_game(self.game)
            liberation_install.setup_last_save_file(self.game.savepath)
            liberation_install.save_config()
        else:
            self.saveGameAs()

    def saveGameAs(self):
        if self.game is not None and self.game.savepath:
            save_dir = self.game.savepath
        else:
            save_dir = str(persistency.save_dir())
        file = QFileDialog.getSaveFileName(
            self,
            "Save As",
            dir=save_dir,
            filter="*.liberation",
        )
        if file is not None:
            self.game.savepath = file[0]
            persistency.save_game(self.game)
            liberation_install.setup_last_save_file(self.game.savepath)
            liberation_install.save_config()

            self.updateWindowTitle(file[0])

    def updateWindowTitle(self, save_path: Optional[str] = None) -> None:
        """
        to DCS Liberation - vX.X.X - file_name
        """
        window_title = f"DCS Liberation - v{VERSION}"
        if save_path:  # appending the file name to title as it is updated
            file_name = save_path.split("/")[-1].split(".liberation")[0]
            window_title = f"{window_title} - {file_name}"
        self.setWindowTitle(window_title)

    def onGameGenerated(self, game: Game):
        self.updateWindowTitle()
        logging.info("On Game generated")
        self.game = game
        GameUpdateSignal.get_instance().game_loaded.emit(self.game)

    def setGame(self, game: Optional[Game]):
        try:
            self.game = game
            if self.info_panel is not None:
                self.info_panel.setGame(game)
            self.sim_controller.set_game(game)
            self.game_model.set(self.game)
        except AttributeError:
            logging.exception("Incompatible save game")
            QMessageBox.critical(
                self,
                "Could not load save game",
                "The save game you have loaded is incompatible with this "
                "version of DCS Liberation.\n"
                "\n"
                f"{traceback.format_exc()}",
                QMessageBox.Ok,
            )
            GameUpdateSignal.get_instance().updateGame(None)
        finally:
            self.enable_game_actions(self.game is not None)

    def showAboutDialog(self):
        text = (
            "<h3>DCS Liberation " + VERSION + "</h3>" +
            "<b>Source code :</b> https://github.com/dcs-liberation/dcs_liberation"
            + "<h4>Authors</h4>" +
            "<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>."
            "<h4>Contributors</h4>" +
            "shdwp, Khopa, ColonelPanic, Roach, Malakhit, Wrycu, calvinmorrow, JohanAberg, Deus, SiKruger, Mustang-25, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57, Plob, Hawkmoon"
            + "<h4>Special Thanks  :</h4>"
            "<b>rp-</b> <i>for the pydcs framework</i><br/>"
            "<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"
            "<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"
            "<b>Walder </b> <i>for the Skynet-IADS script</i><br/>"
            "<b>Anubis Yinepu </b> <i>for the Hercules Cargo script</i><br/>" +
            "<h4>Splash Screen  :</h4>" +
            "Artwork by Andriy Dankovych (CC BY-SA) [https://www.facebook.com/AndriyDankovych]"
        )
        about = QMessageBox()
        about.setWindowTitle("About DCS Liberation")
        about.setIcon(QMessageBox.Icon.Information)
        about.setText(text)
        logging.info(about.textFormat())
        about.exec_()

    def showLiberationDialog(self):
        self.subwindow = QLiberationPreferencesWindow()
        self.subwindow.show()

    def showSettingsDialog(self) -> None:
        self.dialog = QSettingsWindow(self.game)
        self.dialog.show()

    def showStatsDialog(self):
        self.dialog = QStatsWindow(self.game)
        self.dialog.show()

    def showNotesDialog(self):
        self.dialog = QNotesWindow(self.game)
        self.dialog.show()

    def import_templates(self):
        LAYOUTS.import_templates()

    def showLogsDialog(self):
        self.dialog = QLogsWindow()
        self.dialog.show()

    def onDebriefing(self, debrief: Debriefing):
        logging.info("On Debriefing")
        self.debriefing = QDebriefingWindow(debrief)
        self.debriefing.show()

    def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
        QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()

    def open_control_point_info_dialog(self, cp: ControlPoint) -> None:
        self._cp_dialog = QBaseMenu2(None, cp, self.game_model)
        self._cp_dialog.show()

    def _qsettings(self) -> QSettings:
        return QSettings("DCS Liberation", "Qt UI")

    def _restore_window_geometry(self) -> None:
        settings = self._qsettings()
        self.restoreGeometry(settings.value("geometry"))
        self.restoreState(settings.value("windowState"))

    def _save_window_geometry(self) -> None:
        settings = self._qsettings()
        settings.setValue("geometry", self.saveGeometry())
        settings.setValue("windowState", self.saveState())

    def closeEvent(self, event: QCloseEvent) -> None:
        result = QMessageBox.question(
            self,
            "Quit Liberation?",
            "Are you sure you want to quit? All unsaved progress will be lost.",
            QMessageBox.Yes | QMessageBox.No,
        )
        if result == QMessageBox.Yes:
            self._save_window_geometry()
            super().closeEvent(event)
            self.dialog = None
        else:
            event.ignore()
コード例 #5
0
class QLiberationWindow(QMainWindow):
    def __init__(self):
        super(QLiberationWindow, self).__init__()

        self.game: Optional[Game] = None
        self.game_model = GameModel()
        Dialog.set_game(self.game_model)
        self.ato_panel = QAirTaskingOrderPanel(self.game_model)
        self.info_panel = QInfoPanel(self.game)
        self.liberation_map = QLiberationMap(self.game_model)

        self.setGeometry(300, 100, 270, 100)
        self.setWindowTitle(f"DCS Liberation - v{VERSION}")
        self.setWindowIcon(QIcon("./resources/icon.png"))
        self.statusBar().showMessage('Ready')

        self.initUi()
        self.initActions()
        self.initMenuBar()
        self.initToolbar()
        self.connectSignals()

        screen = QDesktopWidget().screenGeometry()
        self.setGeometry(0, 0, screen.width(), screen.height())
        self.setWindowState(Qt.WindowMaximized)

        self.onGameGenerated(persistency.restore_game())

    def initUi(self):
        hbox = QSplitter(Qt.Horizontal)
        vbox = QSplitter(Qt.Vertical)
        hbox.addWidget(self.ato_panel)
        hbox.addWidget(vbox)
        vbox.addWidget(self.liberation_map)
        vbox.addWidget(self.info_panel)

        # Will make the ATO sidebar as small as necessary to fit the content. In
        # practice this means it is sized by the hints in the panel.
        hbox.setSizes([1, 10000000])
        vbox.setSizes([600, 100])

        vbox = QVBoxLayout()
        vbox.setMargin(0)
        vbox.addWidget(QTopPanel(self.game_model))
        vbox.addWidget(hbox)

        central_widget = QWidget()
        central_widget.setLayout(vbox)
        self.setCentralWidget(central_widget)

    def connectSignals(self):
        GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
        GameUpdateSignal.get_instance().debriefingReceived.connect(
            self.onDebriefing)

    def initActions(self):
        self.newGameAction = QAction("&New Game", self)
        self.newGameAction.setIcon(QIcon(CONST.ICONS["New"]))
        self.newGameAction.triggered.connect(self.newGame)
        self.newGameAction.setShortcut('CTRL+N')

        self.openAction = QAction("&Open", self)
        self.openAction.setIcon(QIcon(CONST.ICONS["Open"]))
        self.openAction.triggered.connect(self.openFile)
        self.openAction.setShortcut('CTRL+O')

        self.saveGameAction = QAction("&Save", self)
        self.saveGameAction.setIcon(QIcon(CONST.ICONS["Save"]))
        self.saveGameAction.triggered.connect(self.saveGame)
        self.saveGameAction.setShortcut('CTRL+S')

        self.saveAsAction = QAction("Save &As", self)
        self.saveAsAction.setIcon(QIcon(CONST.ICONS["Save"]))
        self.saveAsAction.triggered.connect(self.saveGameAs)
        self.saveAsAction.setShortcut('CTRL+A')

        self.showAboutDialogAction = QAction("&About DCS Liberation", self)
        self.showAboutDialogAction.setIcon(QIcon.fromTheme("help-about"))
        self.showAboutDialogAction.triggered.connect(self.showAboutDialog)

        self.showLiberationPrefDialogAction = QAction("&Preferences", self)
        self.showLiberationPrefDialogAction.setIcon(
            QIcon.fromTheme("help-about"))
        self.showLiberationPrefDialogAction.triggered.connect(
            self.showLiberationDialog)

    def initToolbar(self):
        self.tool_bar = self.addToolBar("File")
        self.tool_bar.addAction(self.newGameAction)
        self.tool_bar.addAction(self.openAction)
        self.tool_bar.addAction(self.saveGameAction)

    def initMenuBar(self):
        self.menu = self.menuBar()

        file_menu = self.menu.addMenu("&File")
        file_menu.addAction(self.newGameAction)
        file_menu.addAction(self.openAction)
        file_menu.addSeparator()
        file_menu.addAction(self.saveGameAction)
        file_menu.addAction(self.saveAsAction)
        file_menu.addSeparator()
        file_menu.addAction(self.showLiberationPrefDialogAction)
        file_menu.addSeparator()
        file_menu.addAction("E&xit", self.close)

        displayMenu = self.menu.addMenu("&Display")

        last_was_group = True
        for item in DisplayOptions.menu_items():
            if isinstance(item, DisplayRule):
                displayMenu.addAction(self.make_display_rule_action(item))
                last_was_group = False
            elif isinstance(item, DisplayGroup):
                if not last_was_group:
                    displayMenu.addSeparator()
                group = QActionGroup(displayMenu)
                for display_rule in item:
                    displayMenu.addAction(
                        self.make_display_rule_action(display_rule, group))
                last_was_group = True

        help_menu = self.menu.addMenu("&Help")
        help_menu.addAction(
            "&Discord Server", lambda: webbrowser.open_new_tab(
                "https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
        help_menu.addAction(
            "&Github Repository", lambda: webbrowser.open_new_tab(
                "https://github.com/khopa/dcs_liberation"))
        help_menu.addAction(
            "&Releases", lambda: webbrowser.open_new_tab(
                "https://github.com/Khopa/dcs_liberation/releases"))
        help_menu.addAction("&Online Manual",
                            lambda: webbrowser.open_new_tab(URLS["Manual"]))
        help_menu.addAction(
            "&ED Forum Thread",
            lambda: webbrowser.open_new_tab(URLS["ForumThread"]))
        help_menu.addAction("Report an &issue",
                            lambda: webbrowser.open_new_tab(URLS["Issues"]))

        help_menu.addSeparator()
        help_menu.addAction(self.showAboutDialogAction)

    @staticmethod
    def make_display_rule_action(display_rule,
                                 group: Optional[QActionGroup] = None
                                 ) -> QAction:
        def make_check_closure():
            def closure():
                display_rule.value = action.isChecked()

            return closure

        action = QAction(f"&{display_rule.menu_text}", group)
        action.setCheckable(True)
        action.setChecked(display_rule.value)
        action.toggled.connect(make_check_closure())
        return action

    def newGame(self):
        wizard = NewGameWizard(self)
        wizard.show()
        wizard.accepted.connect(
            lambda: self.onGameGenerated(wizard.generatedGame))

    def openFile(self):
        file = QFileDialog.getOpenFileName(
            self,
            "Select game file to open",
            dir=persistency._dcs_saved_game_folder,
            filter="*.liberation")
        if file is not None:
            game = persistency.load_game(file[0])
            GameUpdateSignal.get_instance().updateGame(game)

    def saveGame(self):
        logging.info("Saving game")

        if self.game.savepath:
            persistency.save_game(self.game)
            GameUpdateSignal.get_instance().updateGame(self.game)
        else:
            self.saveGameAs()

    def saveGameAs(self):
        file = QFileDialog.getSaveFileName(
            self,
            "Save As",
            dir=persistency._dcs_saved_game_folder,
            filter="*.liberation")
        if file is not None:
            self.game.savepath = file[0]
            persistency.save_game(self.game)

    def onGameGenerated(self, game: Game):
        logging.info("On Game generated")
        self.game = game
        GameUpdateSignal.get_instance().updateGame(self.game)

    def setGame(self, game: Optional[Game]):
        try:
            if game is not None:
                game.on_load()
            self.game = game
            if self.info_panel is not None:
                self.info_panel.setGame(game)
            self.game_model.set(self.game)
            if self.liberation_map is not None:
                self.liberation_map.setGame(game)
        except AttributeError:
            logging.exception("Incompatible save game")
            QMessageBox.critical(
                self, "Could not load save game",
                "The save game you have loaded is incompatible with this "
                "version of DCS Liberation.\n"
                "\n"
                f"{traceback.format_exc()}", QMessageBox.Ok)
            GameUpdateSignal.get_instance().updateGame(None)

    def showAboutDialog(self):
        text = "<h3>DCS Liberation " + VERSION + "</h3>" + \
               "<b>Source code :</b> https://github.com/khopa/dcs_liberation" + \
               "<h4>Authors</h4>" + \
               "<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \
               "<h4>Contributors</h4>" + \
               "shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57" + \
               "<h4>Special Thanks  :</h4>" \
               "<b>rp-</b> <i>for the pydcs framework</i><br/>"\
               "<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"\
               "<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"
        about = QMessageBox()
        about.setWindowTitle("About DCS Liberation")
        about.setIcon(QMessageBox.Icon.Information)
        about.setText(text)
        logging.info(about.textFormat())
        about.exec_()

    def showLiberationDialog(self):
        self.subwindow = QLiberationPreferencesWindow()
        self.subwindow.show()

    def onDebriefing(self, debrief: DebriefingSignal):
        logging.info("On Debriefing")
        self.debriefing = QDebriefingWindow(debrief.debriefing,
                                            debrief.gameEvent, debrief.game)
        self.debriefing.show()

    def closeEvent(self, event: QCloseEvent) -> None:
        result = QMessageBox.question(
            self, "Quit Liberation?",
            "Are you sure you want to quit? All unsaved progress will be lost.",
            QMessageBox.Yes | QMessageBox.No)
        if result == QMessageBox.Yes:
            super().closeEvent(event)
        else:
            event.ignore()