コード例 #1
0
 def contextMenuEvent(self, event):
     print("Label tries to open menu")
     menu = QMenu()
     menu.addAction("Rename")
     menu.actions()[0].triggered.connect(self.mouseDoubleClickEvent)
     menu.addAction("Set as current object")
     menu.actions()[1].triggered.connect(lambda: self.parent_object.parent_application.set_active_object(self.parent_object))
     menu.addAction("Delete")
     menu.actions()[2].triggered.connect(self.parent_object.remove)
     menu.exec_(event.globalPos())
     del menu
コード例 #2
0
    def createCorrectionsMenu(self, cursor, parent=None):
        """Create and return a menu for correcting the selected word."""
        if not cursor:
            return None

        text = cursor.selectedText()
        suggests = trim_suggestions(text,
                                    self.highlighter.dict().suggest(text),
                                    self.max_suggestions)

        spell_menu = QMenu('Spelling Suggestions', parent)
        for word in suggests:
            action = QAction(word, spell_menu)
            action.setData((cursor, word))
            spell_menu.addAction(action)

        # Only return the menu if it's non-empty
        if spell_menu.actions():
            spell_menu.triggered.connect(self.cb_correct_word)
            return spell_menu

        return None
コード例 #3
0
ファイル: crypt_gui.py プロジェクト: Electrostatus/Various
    def setup(self):
        "constructs the gui"
        Fixed = QSizePolicy()
        MinimumExpanding = QSizePolicy(QSizePolicy.MinimumExpanding,
                                       QSizePolicy.MinimumExpanding)
        self.minKeyLen = 8
        self.maxKeyLen = 4096

        self.splitter = QSplitter(self)
        self.splitter.setOrientation(Qt.Horizontal)
        self.splitter.splitterMoved.connect(self.splitterChanged)

        # left column
        self.leftColumn = QWidget()
        self.vl01 = QVBoxLayout()

        # left column - first item (0; horizonal layout 0)
        self.hl00 = QHBoxLayout()
        self.hl00.setSpacing(5)

        self.openButton = QPushButton('&Open')
        self.openButton.setToolTip('Open folder')
        self.openButton.setMinimumSize(60, 20)
        self.openButton.setMaximumSize(60, 20)
        self.openButton.setSizePolicy(Fixed)
        self.openButton.clicked.connect(self.getFolder)
        #ico = self.style().standardIcon(QStyle.SP_DirIcon)
        #self.openButton.setIcon(ico)

        self.folderLabel = QLabel()
        self.folderLabel.setMinimumSize(135, 20)
        self.folderLabel.setMaximumSize(16777215, 20)
        self.folderLabel.setSizePolicy(MinimumExpanding)
        self.hl00.insertWidget(0, self.openButton)
        self.hl00.insertWidget(1, self.folderLabel)

        # left column - second item (1)
        self.folderTable = QTableWidget()
        self.folderTable.setMinimumSize(200, 32)
        self.folderTable.horizontalHeader().setVisible(False)
        self.folderTable.horizontalHeader().setStretchLastSection(True)
        self.folderTable.verticalHeader().setVisible(False)
        self.folderTable.verticalHeader().setDefaultSectionSize(15)
        self.folderTable.itemChanged.connect(self.editFileName)

        # left column - third item (2)
        self.extraLabel = QLabel()
        self.extraLabel.setMinimumSize(200, 20)
        self.extraLabel.setMaximumSize(16777215, 20)
        self.extraLabel.setSizePolicy(MinimumExpanding)
        self.extraLabel.setTextInteractionFlags(Qt.LinksAccessibleByMouse)

        # finalize left column
        self.vl01.insertLayout(0, self.hl00)
        self.vl01.insertWidget(1, self.folderTable)
        self.vl01.insertWidget(2, self.extraLabel)
        self.leftColumn.setLayout(self.vl01)

        # right column
        self.rightColumn = QWidget()
        self.vl02 = QVBoxLayout()

        # right column - first item (0)
        self.messageLabel = QLabel()
        self.messageLabel.setMinimumSize(290, 20)
        self.messageLabel.setMaximumSize(16777215, 20)
        self.messageLabel.setSizePolicy(MinimumExpanding)
        self.messageLabel.setAlignment(Qt.AlignCenter)

        # right column - second item (2; horizontal layout 1)
        self.hl01 = QHBoxLayout()
        self.hl01.setSpacing(5)

        self.encryptButton = QPushButton('&Encrypt')  #\U0001F512
        self.encryptButton.setToolTip('Encrypt selected file')
        self.encryptButton.setMinimumSize(60, 20)
        self.encryptButton.setMaximumSize(60, 20)
        self.encryptButton.setSizePolicy(Fixed)
        self.encryptButton.clicked.connect(self.encrypt)

        self.encryptPbar = QProgressBar()
        self.encryptPbar.setMinimumSize(225, 20)
        self.encryptPbar.setMaximumSize(16777215, 20)
        self.encryptPbar.setSizePolicy(MinimumExpanding)
        self.encryptPbar.setTextVisible(False)

        palette = self.encryptPbar.palette()  # color of progress bar
        color = QColor(211, 70, 0)
        palette.setColor(QPalette.Highlight, color)
        self.encryptPbar.setPalette(palette)

        self.hl01.insertWidget(0, self.encryptButton)
        self.hl01.insertWidget(1, self.encryptPbar)

        # right column - third item (3; horizontal layout 2)
        self.hl02 = QHBoxLayout()
        self.hl02.setSpacing(5)

        self.cancelButton = QPushButton('C&ANCEL')
        self.cancelButton.setToolTip('Cancels current operation')
        self.cancelButton.setMinimumSize(70, 24)
        self.cancelButton.setMaximumSize(70, 24)
        self.cancelButton.setSizePolicy(Fixed)
        self.cancelButton.clicked.connect(self.setCancel)
        font = self.cancelButton.font()
        font.setBold(True)
        self.cancelButton.setFont(font)
        self.cancelButton.blockSignals(True)
        self.cancelButton.setEnabled(False)
        self.cancelButton.hide()
        self._requestStop = False

        self.keyInput = QLineEdit()
        self.keyInput.setMinimumSize(225, 20)
        self.keyInput.setMaximumSize(16777215, 20)
        self.keyInput.setSizePolicy(MinimumExpanding)
        self.keyInput.setPlaceholderText('key')
        self.keyInput.setMaxLength(self.maxKeyLen)
        self.keyInput.setAlignment(Qt.AlignCenter)
        self.keyInput.textEdited.connect(self.showKeyLen)

        self.genKeyButton = QPushButton('&Gen Key')  #\U0001F511
        self.genKeyButton.setToolTip('Generate a random key')
        self.genKeyButton.setMinimumSize(60, 20)
        self.genKeyButton.setMaximumSize(60, 20)
        self.genKeyButton.setSizePolicy(Fixed)
        self.genKeyButton.clicked.connect(self.genKey)

        self.keySizeSB = QSpinBox()
        self.keySizeSB.setToolTip('Length of key to generate')
        self.keySizeSB.setRange(32, 1024)
        self.keySizeSB.setMinimumSize(40, 20)
        self.keySizeSB.setMaximumSize(40, 20)
        self.keySizeSB.setSizePolicy(Fixed)
        self.keySizeSB.setAlignment(Qt.AlignCenter)
        self.keySizeSB.setButtonSymbols(QSpinBox.NoButtons)
        self.keySizeSB.setWrapping(True)

        self.hl02.insertWidget(0, self.cancelButton)
        self.hl02.insertWidget(1, self.keyInput)
        self.hl02.insertWidget(2, self.genKeyButton)
        self.hl02.insertWidget(3, self.keySizeSB)

        # right column - fourth item (4; horizontal layout 3)
        self.hl03 = QHBoxLayout()
        self.hl03.setSpacing(5)

        self.decryptButton = QPushButton('&Decrypt')  #\U0001F513
        self.decryptButton.setToolTip('Decrypt selected file')
        self.decryptButton.setMinimumSize(60, 20)
        self.decryptButton.setMaximumSize(60, 20)
        self.decryptButton.setSizePolicy(Fixed)
        self.decryptButton.clicked.connect(self.decrypt)

        self.decryptPbar = QProgressBar()
        self.decryptPbar.setMinimumSize(225, 20)
        self.decryptPbar.setMaximumSize(16777215, 20)
        self.decryptPbar.setSizePolicy(MinimumExpanding)
        self.decryptPbar.setTextVisible(False)
        self.decryptPbar.setInvertedAppearance(True)

        palette = self.decryptPbar.palette()  # color of progress bar
        color = QColor(0, 170, 255)
        palette.setColor(QPalette.Highlight, color)
        self.decryptPbar.setPalette(palette)

        self.hl03.insertWidget(0, self.decryptButton)
        self.hl03.insertWidget(1, self.decryptPbar)

        # right column - fifth item (7; horizontal layout 4)
        self.hl04 = QHBoxLayout()
        self.hl04.setSpacing(5)

        self.showKeyCB = QCheckBox('&Show Key')
        self.showKeyCB.setToolTip('Show/Hide key value')
        self.showKeyCB.setMinimumSize(75, 20)
        self.showKeyCB.setMaximumSize(75, 20)
        self.showKeyCB.setSizePolicy(Fixed)
        self.showKeyCB.clicked.connect(self.showKey)
        self.showKeyCB.setChecked(True)

        self.hashPbar = QProgressBar()
        self.hashPbar.setMinimumSize(150, 20)
        self.hashPbar.setMaximumSize(16777215, 20)
        self.hashPbar.setSizePolicy(MinimumExpanding)
        self.hashPbar.setTextVisible(False)

        palette = self.hashPbar.palette()  # color of progress bar
        color = QColor(31, 120, 73)
        palette.setColor(QPalette.Highlight, color)
        self.hashPbar.setPalette(palette)

        self.hashButton = QPushButton('&Hash')
        self.hashButton.setToolTip('Determine file hash')
        self.hashButton.setMinimumSize(60, 20)
        self.hashButton.setMaximumSize(60, 20)
        self.hashButton.setSizePolicy(Fixed)

        menu = QMenu(self.hashButton)
        ico = self.style().standardIcon(QStyle.SP_DialogYesButton)
        for alg in sorted(
                filter(lambda x: 'shake' not in x,
                       hashlib.algorithms_guaranteed),
                key=lambda n:
            (len(n), sorted(hashlib.algorithms_guaranteed).index(n))):
            menu.addAction(
                ico, alg
            )  # drop shake algs as their .hexdigest requires an argument - the rest don't
        menu.addAction(ico, 'Party')
        for i in menu.actions():
            i.setIconVisibleInMenu(False)
        self.hashButton.setMenu(menu)
        menu.triggered.connect(self.genHash)

        self.hl04.insertWidget(0, self.showKeyCB)
        self.hl04.insertWidget(1, self.hashPbar)
        self.hl04.insertWidget(2, self.hashButton)

        # right column - sixth item (8; horizontal layout 5)
        self.hl05 = QHBoxLayout()
        self.hl05.setSpacing(5)

        self.copyButton = QPushButton('&Copy')  #\U0001F4CB
        self.copyButton.setToolTip('Copy key or hash to clipboard')
        self.copyButton.setMinimumSize(60, 20)
        self.copyButton.setMaximumSize(60, 20)
        self.copyButton.setSizePolicy(Fixed)

        menu2 = QMenu(self.copyButton)
        menu2.addAction('Copy Key')
        menu2.addAction('Copy Hash')
        self.copyButton.setMenu(menu2)
        menu2.triggered.connect(self.copyKeyHash)

        self.hashLabel = QLabel()
        self.hashLabel.setMinimumSize(225, 20)
        self.hashLabel.setMaximumSize(16777215, 20)
        self.hashLabel.setSizePolicy(MinimumExpanding)
        self.hashLabel.setTextFormat(Qt.PlainText)
        self.hashLabel.setAlignment(Qt.AlignCenter)
        self.hashLabel.setTextInteractionFlags(Qt.TextSelectableByMouse)

        self.hl05.insertWidget(0, self.copyButton)
        self.hl05.insertWidget(1, self.hashLabel)

        # finalize right column
        self.vl02.insertWidget(0, self.messageLabel)
        self.vl02.insertSpacerItem(1, QSpacerItem(0, 0))
        self.vl02.insertLayout(2, self.hl01)
        self.vl02.insertLayout(3, self.hl02)
        self.vl02.insertLayout(4, self.hl03)
        self.vl02.insertSpacerItem(5, QSpacerItem(0, 0))
        self.vl02.insertWidget(6, QFrame())
        self.vl02.insertLayout(7, self.hl04)
        self.vl02.insertLayout(8, self.hl05)
        self.rightColumn.setLayout(self.vl02)

        # finalize main window
        self.splitter.insertWidget(0, self.leftColumn)
        self.splitter.insertWidget(1, self.rightColumn)

        layout = QHBoxLayout(self)
        layout.addWidget(self.splitter)
        self.setLayout(layout)

        self.setWindowTitle('Simple File Encryptor/Decryptor')
        self.resize(self.sizeHint())
コード例 #4
0
ファイル: MainWindow.py プロジェクト: narfman0/SMB3-Foundry
class MainWindow(QMainWindow):
    def __init__(self, path_to_rom=""):
        super(MainWindow, self).__init__()

        self.setWindowIcon(icon("foundry.ico"))

        file_menu = QMenu("File")

        open_rom_action = file_menu.addAction("&Open ROM")
        open_rom_action.triggered.connect(self.on_open_rom)
        self.open_m3l_action = file_menu.addAction("&Open M3L")
        self.open_m3l_action.triggered.connect(self.on_open_m3l)

        file_menu.addSeparator()

        self.save_rom_action = file_menu.addAction("&Save ROM")
        self.save_rom_action.triggered.connect(self.on_save_rom)
        self.save_rom_as_action = file_menu.addAction("&Save ROM as ...")
        self.save_rom_as_action.triggered.connect(self.on_save_rom_as)
        """
        file_menu.AppendSeparator()
        """
        self.save_m3l_action = file_menu.addAction("&Save M3L")
        self.save_m3l_action.triggered.connect(self.on_save_m3l)
        """
        file_menu.Append(ID_SAVE_LEVEL_TO, "&Save Level to", "")
        file_menu.AppendSeparator()
        file_menu.Append(ID_APPLY_IPS_PATCH, "&Apply IPS Patch", "")
        file_menu.AppendSeparator()
        file_menu.Append(ID_ROM_PRESET, "&ROM Preset", "")
        """
        file_menu.addSeparator()
        settings_action = file_menu.addAction("&Settings")
        settings_action.triggered.connect(show_settings)
        file_menu.addSeparator()
        exit_action = file_menu.addAction("&Exit")
        exit_action.triggered.connect(lambda _: self.close())

        self.menuBar().addMenu(file_menu)
        """
        edit_menu = wx.Menu()

        edit_menu.Append(ID_EDIT_LEVEL, "&Edit Level", "")
        edit_menu.Append(ID_EDIT_OBJ_DEFS, "&Edit Object Definitions", "")
        edit_menu.Append(ID_EDIT_PALETTE, "&Edit Palette", "")
        edit_menu.Append(ID_EDIT_GRAPHICS, "&Edit Graphics", "")
        edit_menu.Append(ID_EDIT_MISC, "&Edit Miscellaneous", "")
        edit_menu.AppendSeparator()
        edit_menu.Append(ID_FREE_FORM_MODE, "&Free form Mode", "")
        edit_menu.Append(ID_LIMIT_SIZE, "&Limit Size", "")
        """

        self.level_menu = QMenu("Level")

        self.select_level_action = self.level_menu.addAction("&Select Level")
        self.select_level_action.triggered.connect(self.open_level_selector)

        self.reload_action = self.level_menu.addAction("&Reload Level")
        self.reload_action.triggered.connect(self.reload_level)
        self.level_menu.addSeparator()
        self.edit_header_action = self.level_menu.addAction("&Edit Header")
        self.edit_header_action.triggered.connect(self.on_header_editor)
        self.edit_autoscroll = self.level_menu.addAction("Edit Autoscrolling")
        self.edit_autoscroll.triggered.connect(self.on_edit_autoscroll)

        self.menuBar().addMenu(self.level_menu)

        self.object_menu = QMenu("Objects")

        view_blocks_action = self.object_menu.addAction("&View Blocks")
        view_blocks_action.triggered.connect(self.on_block_viewer)
        view_objects_action = self.object_menu.addAction("&View Objects")
        view_objects_action.triggered.connect(self.on_object_viewer)
        self.object_menu.addSeparator()
        view_palettes_action = self.object_menu.addAction(
            "View Object Palettes")
        view_palettes_action.triggered.connect(self.on_palette_viewer)

        self.menuBar().addMenu(self.object_menu)

        self.view_menu = QMenu("View")
        self.view_menu.triggered.connect(self.on_menu)

        action = self.view_menu.addAction("Mario")
        action.setProperty(ID_PROP, ID_MARIO)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_mario"])

        action = self.view_menu.addAction("&Jumps on objects")
        action.setProperty(ID_PROP, ID_JUMP_OBJECTS)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_jump_on_objects"])

        action = self.view_menu.addAction("Items in blocks")
        action.setProperty(ID_PROP, ID_ITEM_BLOCKS)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_items_in_blocks"])

        action = self.view_menu.addAction("Invisible items")
        action.setProperty(ID_PROP, ID_INVISIBLE_ITEMS)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_invisible_items"])

        action = self.view_menu.addAction("Autoscroll Path")
        action.setProperty(ID_PROP, ID_AUTOSCROLL)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_autoscroll"])

        self.view_menu.addSeparator()

        action = self.view_menu.addAction("Jump Zones")
        action.setProperty(ID_PROP, ID_JUMPS)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_jumps"])

        action = self.view_menu.addAction("&Grid lines")
        action.setProperty(ID_PROP, ID_GRID_LINES)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_grid"])

        action = self.view_menu.addAction("Resize Type")
        action.setProperty(ID_PROP, ID_RESIZE_TYPE)
        action.setCheckable(True)
        action.setChecked(SETTINGS["draw_expansion"])

        self.view_menu.addSeparator()

        action = self.view_menu.addAction("&Block Transparency")
        action.setProperty(ID_PROP, ID_TRANSPARENCY)
        action.setCheckable(True)
        action.setChecked(SETTINGS["block_transparency"])

        self.view_menu.addSeparator()
        self.view_menu.addAction(
            "&Save Screenshot of Level").triggered.connect(self.on_screenshot)
        """
        self.view_menu.Append(ID_BACKGROUND_FLOOR, "&Background & Floor", "")
        self.view_menu.Append(ID_TOOLBAR, "&Toolbar", "")
        self.view_menu.AppendSeparator()
        self.view_menu.Append(ID_ZOOM, "&Zoom", "")
        self.view_menu.AppendSeparator()
        self.view_menu.Append(ID_USE_ROM_GRAPHICS, "&Use ROM Graphics", "")
        self.view_menu.Append(ID_PALETTE, "&Palette", "")
        self.view_menu.AppendSeparator()
        self.view_menu.Append(ID_MORE, "&More", "")
        """

        self.menuBar().addMenu(self.view_menu)

        help_menu = QMenu("Help")
        """
        help_menu.Append(ID_ENEMY_COMPATIBILITY, "&Enemy Compatibility", "")
        help_menu.Append(ID_TROUBLESHOOTING, "&Troubleshooting", "")
        help_menu.AppendSeparator()
        help_menu.Append(ID_PROGRAM_WEBSITE, "&Program Website", "")
        help_menu.Append(ID_MAKE_A_DONATION, "&Make a Donation", "")
        help_menu.AppendSeparator()
        """
        update_action = help_menu.addAction("Check for updates")
        update_action.triggered.connect(self.on_check_for_update)

        help_menu.addSeparator()

        video_action = help_menu.addAction("Feature Video on YouTube")
        video_action.triggered.connect(lambda: open_url(feature_video_link))

        github_action = help_menu.addAction("Github Repository")
        github_action.triggered.connect(lambda: open_url(github_link))

        discord_action = help_menu.addAction("SMB3 Rom Hacking Discord")
        discord_action.triggered.connect(lambda: open_url(discord_link))

        help_menu.addSeparator()

        about_action = help_menu.addAction("&About")
        about_action.triggered.connect(self.on_about)

        self.menuBar().addMenu(help_menu)

        self.block_viewer = None
        self.object_viewer = None

        self.level_ref = LevelRef()
        self.level_ref.data_changed.connect(self._on_level_data_changed)

        self.context_menu = ContextMenu(self.level_ref)
        self.context_menu.triggered.connect(self.on_menu)

        self.level_view = LevelView(self, self.level_ref, self.context_menu)

        self.scroll_panel = QScrollArea()
        self.scroll_panel.setWidgetResizable(True)
        self.scroll_panel.setWidget(self.level_view)

        self.setCentralWidget(self.scroll_panel)

        self.spinner_panel = SpinnerPanel(self, self.level_ref)
        self.spinner_panel.zoom_in_triggered.connect(self.level_view.zoom_in)
        self.spinner_panel.zoom_out_triggered.connect(self.level_view.zoom_out)
        self.spinner_panel.object_change.connect(self.on_spin)

        self.object_list = ObjectList(self, self.level_ref, self.context_menu)

        self.object_dropdown = ObjectDropdown(self)
        self.object_dropdown.object_selected.connect(
            self._on_placeable_object_selected)

        self.level_size_bar = LevelSizeBar(self, self.level_ref)
        self.enemy_size_bar = EnemySizeBar(self, self.level_ref)

        self.jump_list = JumpList(self, self.level_ref)
        self.jump_list.add_jump.connect(self.on_jump_added)
        self.jump_list.edit_jump.connect(self.on_jump_edit)
        self.jump_list.remove_jump.connect(self.on_jump_removed)

        splitter = QSplitter(self)
        splitter.setOrientation(Qt.Vertical)

        splitter.addWidget(self.object_list)
        splitter.setStretchFactor(0, 1)
        splitter.addWidget(self.jump_list)

        splitter.setChildrenCollapsible(False)

        level_toolbar = QToolBar("Level Info Toolbar", self)
        level_toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
        level_toolbar.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        level_toolbar.setOrientation(Qt.Horizontal)
        level_toolbar.setFloatable(False)

        level_toolbar.addWidget(self.spinner_panel)
        level_toolbar.addWidget(self.object_dropdown)
        level_toolbar.addWidget(self.level_size_bar)
        level_toolbar.addWidget(self.enemy_size_bar)
        level_toolbar.addWidget(splitter)

        level_toolbar.setAllowedAreas(Qt.LeftToolBarArea | Qt.RightToolBarArea)

        self.addToolBar(Qt.RightToolBarArea, level_toolbar)

        self.object_toolbar = ObjectToolBar(self)
        self.object_toolbar.object_selected.connect(
            self._on_placeable_object_selected)

        object_toolbar = QToolBar("Object Toolbar", self)
        object_toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
        object_toolbar.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
        object_toolbar.setFloatable(False)

        object_toolbar.addWidget(self.object_toolbar)
        object_toolbar.setAllowedAreas(Qt.LeftToolBarArea
                                       | Qt.RightToolBarArea)

        self.addToolBar(Qt.LeftToolBarArea, object_toolbar)

        self.menu_toolbar = QToolBar("Menu Toolbar", self)
        self.menu_toolbar.setOrientation(Qt.Horizontal)
        self.menu_toolbar.setIconSize(QSize(20, 20))

        self.menu_toolbar.addAction(
            icon("settings.svg"),
            "Editor Settings").triggered.connect(show_settings)
        self.menu_toolbar.addSeparator()
        self.menu_toolbar.addAction(
            icon("folder.svg"), "Open ROM").triggered.connect(self.on_open_rom)
        self.menu_toolbar.addAction(
            icon("save.svg"), "Save Level").triggered.connect(self.on_save_rom)
        self.menu_toolbar.addSeparator()

        self.undo_action = self.menu_toolbar.addAction(icon("rotate-ccw.svg"),
                                                       "Undo Action")
        self.undo_action.triggered.connect(self.level_ref.undo)
        self.undo_action.setEnabled(False)
        self.redo_action = self.menu_toolbar.addAction(icon("rotate-cw.svg"),
                                                       "Redo Action")
        self.redo_action.triggered.connect(self.level_ref.redo)
        self.redo_action.setEnabled(False)

        self.menu_toolbar.addSeparator()
        play_action = self.menu_toolbar.addAction(icon("play-circle.svg"),
                                                  "Play Level")
        play_action.triggered.connect(self.on_play)
        play_action.setWhatsThis(
            "Opens an emulator with the current Level set to 1-1.\nSee Settings."
        )
        self.menu_toolbar.addSeparator()
        self.menu_toolbar.addAction(icon("zoom-out.svg"),
                                    "Zoom Out").triggered.connect(
                                        self.level_view.zoom_out)
        self.menu_toolbar.addAction(icon("zoom-in.svg"),
                                    "Zoom In").triggered.connect(
                                        self.level_view.zoom_in)
        self.menu_toolbar.addSeparator()
        header_action = self.menu_toolbar.addAction(icon("tool.svg"),
                                                    "Edit Level Header")
        header_action.triggered.connect(self.on_header_editor)
        header_action.setWhatsThis(
            "<b>Header Editor</b><br/>"
            "Many configurations regarding the level are done in its header, like the length of "
            "the timer, or where and how Mario enters the level.<br/>")

        self.jump_destination_action = self.menu_toolbar.addAction(
            icon("arrow-right-circle.svg"), "Go to Jump Destination")
        self.jump_destination_action.triggered.connect(
            self._go_to_jump_destination)
        self.jump_destination_action.setWhatsThis(
            "Opens the level, that can be reached from this one, e.g. by entering a pipe."
        )

        self.menu_toolbar.addSeparator()

        whats_this_action = QWhatsThis.createAction()
        whats_this_action.setWhatsThis(
            "Click on parts of the editor, to receive help information.")
        whats_this_action.setIcon(icon("help-circle.svg"))
        whats_this_action.setText("Starts 'What's this?' mode")
        self.menu_toolbar.addAction(whats_this_action)

        self.menu_toolbar.addSeparator()
        self.warning_list = WarningList(self, self.level_ref)

        warning_action = self.menu_toolbar.addAction(
            icon("alert-triangle.svg"), "Warning Panel")
        warning_action.setWhatsThis("Shows a list of warnings.")
        warning_action.triggered.connect(self.warning_list.show)
        warning_action.setDisabled(True)

        self.warning_list.warnings_updated.connect(warning_action.setEnabled)

        self.addToolBar(Qt.TopToolBarArea, self.menu_toolbar)

        self.status_bar = ObjectStatusBar(self, self.level_ref)
        self.setStatusBar(self.status_bar)

        self.delete_shortcut = QShortcut(QKeySequence(Qt.Key_Delete), self,
                                         self.remove_selected_objects)

        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_X), self, self._cut_objects)
        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_C), self, self._copy_objects)
        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_V), self, self._paste_objects)

        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Z), self, self.level_ref.undo)
        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Y), self, self.level_ref.redo)
        QShortcut(QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_Z), self,
                  self.level_ref.redo)

        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Plus), self,
                  self.level_view.zoom_in)
        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Minus), self,
                  self.level_view.zoom_out)

        QShortcut(QKeySequence(Qt.CTRL + Qt.Key_A), self,
                  self.level_view.select_all)

        self.on_open_rom(path_to_rom)

        self.showMaximized()

    def _on_level_data_changed(self):
        self.undo_action.setEnabled(self.level_ref.undo_stack.undo_available)
        self.redo_action.setEnabled(self.level_ref.undo_stack.redo_available)

        self.jump_destination_action.setEnabled(
            self.level_ref.level.has_next_area)

    def _go_to_jump_destination(self):
        if not self.safe_to_change():
            return

        level_address = self.level_ref.level.next_area_objects
        enemy_address = self.level_ref.level.next_area_enemies + 1
        object_set = self.level_ref.level.next_area_object_set

        world, level = world_and_level_for_level_address(level_address)

        self.update_level(f"Level {world}-{level}", level_address,
                          enemy_address, object_set)

    def on_play(self):
        """
        Copies the ROM, including the current level, to a temporary directory, saves the current level as level 1-1 and
        opens the rom in an emulator.
        """
        temp_dir = pathlib.Path(tempfile.gettempdir()) / "smb3foundry"
        temp_dir.mkdir(parents=True, exist_ok=True)

        path_to_temp_rom = temp_dir / "instaplay.rom"

        ROM().save_to(path_to_temp_rom)

        if not self._put_current_level_to_level_1_1(path_to_temp_rom):
            return

        if not self._set_default_powerup(path_to_temp_rom):
            return

        arguments = SETTINGS["instaplay_arguments"].replace(
            "%f", str(path_to_temp_rom))
        arguments = shlex.split(arguments, posix=False)

        emu_path = pathlib.Path(SETTINGS["instaplay_emulator"])

        if emu_path.is_absolute():
            if emu_path.exists():
                emulator = str(emu_path)
            else:
                QMessageBox.critical(
                    self, "Emulator not found",
                    f"Check it under File > Settings.\nFile {emu_path} not found."
                )
                return
        else:
            emulator = SETTINGS["instaplay_emulator"]

        try:
            subprocess.run([emulator, *arguments])
        except Exception as e:
            QMessageBox.critical(self, "Emulator command failed.",
                                 f"Check it under File > Settings.\n{str(e)}")

    def _open_rom(self, path_to_rom):
        with open(path_to_rom, "rb") as smb3_rom:
            data = smb3_rom.read()

        rom = SMB3Rom(bytearray(data))
        return rom

    def _put_current_level_to_level_1_1(self, path_to_rom) -> bool:
        rom = self._open_rom(path_to_rom)

        # load world-1 data
        world_1 = SMB3World.from_world_number(rom, 1)

        # find position of "level 1" tile in world map
        for position in world_1.gen_positions():
            if position.tile() == TILE_LEVEL_1:
                break
        else:
            QMessageBox.critical(
                self, "Couldn't place level",
                "Could not find a level 1 tile in World 1 to put your level at."
            )
            return False

        if not self.level_ref.level.attached_to_rom:
            QMessageBox.critical(
                self,
                "Couldn't place level",
                "The Level is not part of the rom yet (M3L?). Try saving it into the ROM first.",
            )
            return False

        # write level and enemy data of current level
        (layout_address,
         layout_bytes), (enemy_address,
                         enemy_bytes) = self.level_ref.level.to_bytes()
        rom.write(layout_address, layout_bytes)
        rom.write(enemy_address, enemy_bytes)

        # replace level information with that of current level
        object_set_number = self.level_ref.object_set_number

        world_1.replace_level_at_position(
            (layout_address, enemy_address - 1, object_set_number), position)

        # save rom
        rom.save_to(path_to_rom)

        return True

    def _set_default_powerup(self, path_to_rom) -> bool:
        rom = self._open_rom(path_to_rom)

        *_, powerup, hasPWing = POWERUPS[SETTINGS["default_powerup"]]

        rom.write(Title_PrepForWorldMap + 0x1, bytes([powerup]))

        nop = 0xEA
        rts = 0x60
        lda = 0xA9
        staAbsolute = 0x8D

        # If a P-wing powerup is selected, another variable needs to be set with the P-wing value
        # This piece of code overwrites a part of Title_DebugMenu
        if hasPWing:
            Map_Power_DispHigh = 0x03
            Map_Power_DispLow = 0xF3

            # We need to start one byte before Title_DebugMenu to remove the RTS of Title_PrepForWorldMap
            # The assembly code below reads as follows:
            # LDA 0x08
            # STA $03F3
            # RTS
            rom.write(
                Title_DebugMenu - 0x1,
                bytes([
                    lda,
                    0x8,
                    staAbsolute,
                    Map_Power_DispLow,
                    Map_Power_DispHigh,
                    # The RTS to get out of the now extended Title_PrepForWorldMap
                    rts,
                ]),
            )

            # Remove code that resets the powerup value by replacing it with no-operations
            # Otherwise this code would copy the value of the normal powerup here
            # (So if the powerup would be Raccoon Mario, Map_Power_Disp would also be
            # set as Raccoon Mario instead of P-wing
            Map_Power_DispResetLocation = 0x3C5A2
            rom.write(Map_Power_DispResetLocation, bytes([nop, nop, nop]))

        rom.save_to(path_to_rom)
        return True

    def on_screenshot(self, _) -> bool:
        if self.level_view is None:
            return False

        recommended_file = f"{os.path.expanduser('~')}/{ROM.name} - {self.level_view.level_ref.name}.png"

        pathname, _ = QFileDialog.getSaveFileName(self,
                                                  caption="Save Screenshot",
                                                  dir=recommended_file,
                                                  filter=IMG_FILE_FILTER)

        if not pathname:
            return False

        # Proceed loading the file chosen by the user
        self.level_view.make_screenshot().save(pathname)

        return True

    def update_title(self):
        if self.level_view.level_ref is not None and ROM is not None:
            title = f"{self.level_view.level_ref.name} - {ROM.name}"
        else:
            title = "SMB3Foundry"

        self.setWindowTitle(title)

    def on_open_rom(self, path_to_rom="") -> bool:
        if not self.safe_to_change():
            return False

        if not path_to_rom:
            # otherwise ask the user what new file to open
            path_to_rom, _ = QFileDialog.getOpenFileName(
                self, caption="Open ROM", filter=ROM_FILE_FILTER)

            if not path_to_rom:
                self._enable_disable_gui_elements()
                return False

        # Proceed loading the file chosen by the user
        try:
            ROM.load_from_file(path_to_rom)

            return self.open_level_selector(None)

        except IOError as exp:
            QMessageBox.warning(self,
                                type(exp).__name__,
                                f"Cannot open file '{path_to_rom}'.")
            return False
        finally:
            self._enable_disable_gui_elements()

    def on_open_m3l(self, _) -> bool:
        if not self.safe_to_change():
            return False

        # otherwise ask the user what new file to open
        pathname, _ = QFileDialog.getOpenFileName(self,
                                                  caption="Open M3L file",
                                                  filter=M3L_FILE_FILTER)

        if not pathname:
            return False

        # Proceed loading the file chosen by the user
        try:
            with open(pathname, "rb") as m3l_file:

                self.level_view.from_m3l(bytearray(m3l_file.read()))
        except IOError as exp:
            QMessageBox.warning(self,
                                type(exp).__name__,
                                f"Cannot open file '{pathname}'.")

            return False

        self.level_view.level_ref.name = os.path.basename(pathname)

        self.update_gui_for_level()

        return True

    def safe_to_change(self) -> bool:
        if not self.level_ref:
            return True

        if self.level_ref.level.changed:
            answer = QMessageBox.question(
                self,
                "Please confirm",
                "Current content has not been saved! Proceed?",
                QMessageBox.No | QMessageBox.Yes,
                QMessageBox.No,
            )

            return answer == QMessageBox.Yes
        else:
            return True

    def on_save_rom(self, _):
        self.save_rom(False)

    def on_save_rom_as(self, _):
        self.save_rom(True)

    def save_rom(self, is_save_as):
        safe_to_save, reason, additional_info = self.level_view.level_safe_to_save(
        )

        if not safe_to_save:
            answer = QMessageBox.warning(
                self,
                reason,
                f"{additional_info}\n\nDo you want to proceed?",
                QMessageBox.No | QMessageBox.Yes,
                QMessageBox.No,
            )

            if answer == QMessageBox.No:
                return

        if not self.level_ref.attached_to_rom:
            QMessageBox.information(
                self,
                "Importing M3L into ROM",
                "Please select the positions in the ROM you want the level objects and enemies/items to be stored.",
                QMessageBox.Ok,
            )

            level_selector = LevelSelector(self)

            answer = level_selector.exec_()

            if answer == QMessageBox.Accepted:
                self.level_view.level_ref.attach_to_rom(
                    level_selector.object_data_offset,
                    level_selector.enemy_data_offset)

                if is_save_as:
                    # if we save to another rom, don't consider the level
                    # attached (to the current rom)
                    self.level_view.level_ref.attached_to_rom = False
            else:
                return

        if is_save_as:
            pathname, _ = QFileDialog.getSaveFileName(self,
                                                      caption="Save ROM as",
                                                      filter=ROM_FILE_FILTER)
            if not pathname:
                return  # the user changed their mind
        else:
            pathname = ROM.path

        level = self.level_ref.level

        for offset, data in level.to_bytes():
            ROM().bulk_write(data, offset)

        try:
            ROM().save_to_file(pathname)
        except IOError as exp:
            QMessageBox.warning(self, f"{type(exp).__name__}",
                                f"Cannot save ROM data to file '{pathname}'.")

        self.update_title()

        if not is_save_as:
            level.changed = False

    def on_save_m3l(self, _):
        suggested_file = self.level_view.level_ref.name

        if not suggested_file.endswith(".m3l"):
            suggested_file += ".m3l"

        pathname, _ = QFileDialog.getSaveFileName(self,
                                                  caption="Save M3L as",
                                                  filter=M3L_FILE_FILTER)

        if not pathname:
            return

        level = self.level_view.level_ref

        try:
            with open(pathname, "wb") as m3l_file:
                m3l_file.write(level.to_m3l())
        except IOError as exp:
            QMessageBox.warning(self,
                                type(exp).__name__,
                                f"Couldn't save level to '{pathname}'.")

    def on_check_for_update(self):
        self.setCursor(Qt.WaitCursor)

        current_version = get_current_version_name()

        try:
            latest_version = get_latest_version_name()
        except ValueError as ve:
            QMessageBox.critical(self, "Error while checking for updates",
                                 f"Error: {ve}")
            return

        if current_version != latest_version:
            latest_release_url = f"{releases_link}/tag/{latest_version}"

            go_to_github_button = QPushButton(icon("external-link.svg"),
                                              "Go to latest release")
            go_to_github_button.clicked.connect(
                lambda: open_url(latest_release_url))

            info_box = QMessageBox(
                QMessageBox.Information, "New release available",
                f"New Version {latest_version} is available.")

            info_box.addButton(QMessageBox.Cancel)
            info_box.addButton(go_to_github_button, QMessageBox.AcceptRole)

            info_box.exec_()
        else:
            QMessageBox.information(
                self, "No newer release",
                f"Version {current_version} is up to date.")

        self.setCursor(Qt.ArrowCursor)

    def on_menu(self, action: QAction):
        item_id = action.property(ID_PROP)

        if item_id in CHECKABLE_MENU_ITEMS:
            self.on_menu_item_checked(action)
            self.level_view.update()

            # if setting a checkbox, keep the menu open
            menu_of_action: QMenu = self.sender()
            menu_of_action.exec_()

        elif item_id in self.context_menu.get_all_menu_item_ids():
            x, y = self.context_menu.get_position()

            if item_id == CMAction.REMOVE:
                self.remove_selected_objects()
            elif item_id == CMAction.ADD_OBJECT:
                selected_object = self.object_dropdown.currentIndex()

                if selected_object != -1:
                    self.place_object_from_dropdown((x, y))
                else:
                    self.create_object_at(x, y)

            elif item_id == CMAction.CUT:
                self._cut_objects()
            elif item_id == CMAction.COPY:
                self._copy_objects()
            elif item_id == CMAction.PASTE:
                self._paste_objects(x, y)
            elif item_id == CMAction.FOREGROUND:
                self.bring_objects_to_foreground()
            elif item_id == CMAction.BACKGROUND:
                self.bring_objects_to_background()

        self.level_view.update()

    def reload_level(self):
        if not self.safe_to_change():
            return

        level_name = self.level_view.level_ref.name
        object_data = self.level_view.level_ref.header_offset
        enemy_data = self.level_view.level_ref.enemy_offset
        object_set = self.level_view.level_ref.object_set_number

        self.update_level(level_name, object_data, enemy_data, object_set)

    def _on_placeable_object_selected(self, level_object: Union[LevelObject,
                                                                EnemyObject]):
        if self.sender() is self.object_toolbar:
            self.object_dropdown.select_object(level_object)
        else:
            self.object_toolbar.select_object(level_object)

    @undoable
    def bring_objects_to_foreground(self):
        self.level_ref.level.bring_to_foreground(
            self.level_ref.selected_objects)

    @undoable
    def bring_objects_to_background(self):
        self.level_ref.level.bring_to_background(
            self.level_ref.selected_objects)

    @undoable
    def create_object_at(self, x, y):
        self.level_view.create_object_at(x, y)

    @undoable
    def create_enemy_at(self, x, y):
        self.level_view.create_enemy_at(x, y)

    def _cut_objects(self):
        self._copy_objects()
        self.remove_selected_objects()

    def _copy_objects(self):
        selected_objects = self.level_view.get_selected_objects().copy()

        if selected_objects:
            self.context_menu.set_copied_objects(selected_objects)

    @undoable
    def _paste_objects(self, x=None, y=None):
        self.level_view.paste_objects_at(
            self.context_menu.get_copied_objects(), x, y)

    @undoable
    def remove_selected_objects(self):
        self.level_view.remove_selected_objects()
        self.level_view.update()
        self.spinner_panel.disable_all()

    def on_menu_item_checked(self, action: QAction):
        item_id = action.property(ID_PROP)

        checked = action.isChecked()

        if item_id == ID_GRID_LINES:
            self.level_view.draw_grid = checked
        elif item_id == ID_TRANSPARENCY:
            self.level_view.transparency = checked
        elif item_id == ID_JUMPS:
            self.level_view.draw_jumps = checked
        elif item_id == ID_MARIO:
            self.level_view.draw_mario = checked
        elif item_id == ID_RESIZE_TYPE:
            self.level_view.draw_expansions = checked
        elif item_id == ID_JUMP_OBJECTS:
            self.level_view.draw_jumps_on_objects = checked
        elif item_id == ID_ITEM_BLOCKS:
            self.level_view.draw_items_in_blocks = checked
        elif item_id == ID_INVISIBLE_ITEMS:
            self.level_view.draw_invisible_items = checked
        elif item_id == ID_AUTOSCROLL:
            self.level_view.draw_autoscroll = checked

        SETTINGS["draw_mario"] = self.level_view.draw_mario
        SETTINGS["draw_jumps"] = self.level_view.draw_jumps
        SETTINGS["draw_grid"] = self.level_view.draw_grid
        SETTINGS["draw_expansion"] = self.level_view.draw_expansions
        SETTINGS[
            "draw_jump_on_objects"] = self.level_view.draw_jumps_on_objects
        SETTINGS["draw_items_in_blocks"] = self.level_view.draw_items_in_blocks
        SETTINGS["draw_invisible_items"] = self.level_view.draw_invisible_items
        SETTINGS["draw_autoscroll"] = self.level_view.draw_autoscroll
        SETTINGS["block_transparency"] = self.level_view.transparency

        save_settings()

    @undoable
    def on_spin(self, _):
        selected_objects = self.level_ref.selected_objects

        if len(selected_objects) != 1:
            logging.error(selected_objects, RuntimeWarning)
            return

        selected_object = selected_objects[0]

        obj_type = self.spinner_panel.get_type()

        if isinstance(selected_object, LevelObject):
            domain = self.spinner_panel.get_domain()

            if selected_object.is_4byte:
                length = self.spinner_panel.get_length()
            else:
                length = None

            self.level_view.replace_object(selected_object, domain, obj_type,
                                           length)
        else:
            self.level_view.replace_enemy(selected_object, obj_type)

        self.level_ref.data_changed.emit()

    def fill_object_list(self):
        self.object_list.Clear()

        self.object_list.SetItems(self.level_view.get_object_names())

    def open_level_selector(self, _):
        if not self.safe_to_change():
            return

        level_selector = LevelSelector(self)

        level_was_selected = level_selector.exec_() == QDialog.Accepted

        if level_was_selected:
            self.update_level(
                level_selector.level_name,
                level_selector.object_data_offset,
                level_selector.enemy_data_offset,
                level_selector.object_set,
            )

        return level_was_selected

    def on_block_viewer(self, _):
        if self.block_viewer is None:
            self.block_viewer = BlockViewer(parent=self)

        if self.level_ref.level is not None:
            self.block_viewer.object_set = self.level_ref.object_set.number
            self.block_viewer.palette_group = self.level_ref.object_palette_index

        self.block_viewer.show()

    def on_object_viewer(self, _):
        if self.object_viewer is None:
            self.object_viewer = ObjectViewer(parent=self)

        if self.level_ref.level is not None:
            object_set = self.level_ref.object_set.number
            graphics_set = self.level_ref.graphic_set

            self.object_viewer.set_object_and_graphic_set(
                object_set, graphics_set)

            if len(self.level_view.get_selected_objects()) == 1:
                selected_object = self.level_view.get_selected_objects()[0]

                if isinstance(selected_object, LevelObject):
                    self.object_viewer.set_object(selected_object.domain,
                                                  selected_object.obj_index,
                                                  selected_object.length)

        self.object_viewer.show()

    def on_palette_viewer(self, _):
        PaletteViewer(self, self.level_ref).exec_()

    def on_edit_autoscroll(self, _):
        AutoScrollEditor(self, self.level_ref).exec_()

    def on_header_editor(self, _):
        HeaderEditor(self, self.level_ref).exec_()

    def update_level(self, level_name: str, object_data_offset: int,
                     enemy_data_offset: int, object_set: int):
        try:
            self.level_ref.load_level(level_name, object_data_offset,
                                      enemy_data_offset, object_set)
        except IndexError:
            QMessageBox.critical(
                self, "Please confirm",
                "Failed loading level. The level offsets don't match.")

            return

        self.update_gui_for_level()

    def update_gui_for_level(self):
        self._enable_disable_gui_elements()

        self.update_title()
        self.jump_list.update()

        is_a_world_map = isinstance(self.level_ref.level, WorldMap)

        self.save_m3l_action.setEnabled(not is_a_world_map)
        self.edit_header_action.setEnabled(not is_a_world_map)

        if is_a_world_map:
            self.object_dropdown.Clear()
            self.object_dropdown.setEnabled(False)

            self.jump_list.setEnabled(False)
            self.jump_list.Clear()
        else:
            self.object_dropdown.setEnabled(True)
            self.object_dropdown.set_object_set(
                self.level_ref.object_set_number, self.level_ref.graphic_set)

            self.jump_list.setEnabled(True)

        self.object_toolbar.set_object_set(self.level_ref.object_set_number,
                                           self.level_ref.graphic_set)

        self.level_view.update()

    def _enable_disable_gui_elements(self):
        rom_elements = [
            # entries in file menu
            self.open_m3l_action,
            self.save_rom_action,
            self.save_rom_as_action,
            # entry in level menu
            self.select_level_action,
        ]

        level_elements = [
            # entry in file menu
            self.save_m3l_action,
            # top toolbar
            self.menu_toolbar,
            # other gui elements
            self.level_view,
            self.spinner_panel,
            self.object_toolbar,
            self.level_size_bar,
            self.enemy_size_bar,
            self.object_list,
            self.jump_list,
            self.object_toolbar,
        ]

        level_elements.extend(self.level_menu.actions())
        level_elements.remove(self.select_level_action)

        level_elements.extend(self.object_menu.actions())
        level_elements.extend(self.view_menu.actions())

        for gui_element in rom_elements:
            gui_element.setEnabled(ROM.is_loaded())

        for gui_element in level_elements:
            gui_element.setEnabled(ROM.is_loaded()
                                   and self.level_ref.level is not None)

    def on_jump_edit(self):
        index = self.jump_list.currentIndex().row()

        updated_jump = JumpEditor.edit_jump(
            self, self.level_view.level_ref.jumps[index])

        self.on_jump_edited(updated_jump)

    @undoable
    def on_jump_added(self):
        self.level_view.add_jump()

    @undoable
    def on_jump_removed(self):
        self.level_view.remove_jump(self.jump_list.currentIndex().row())

    @undoable
    def on_jump_edited(self, jump):
        index = self.jump_list.currentIndex().row()

        assert index >= 0

        if isinstance(self.level_ref.level, Level):
            self.level_view.level_ref.jumps[index] = jump
            self.jump_list.item(index).setText(str(jump))

    def on_jump_list_change(self, event):
        self.jump_list.set_jumps(event)

    def mouseReleaseEvent(self, event: QMouseEvent):
        if event.button() == Qt.MiddleButton:
            pos = self.level_view.mapFromGlobal(self.mapToGlobal(
                event.pos())).toTuple()

            self.place_object_from_dropdown(pos)

    @undoable
    def place_object_from_dropdown(self, pos: Tuple[int, int]) -> None:
        # the dropdown is synchronized with the toolbar, so it doesn't matter where to take it from
        level_object = self.object_dropdown.currentData(Qt.UserRole)

        self.object_toolbar.add_recent_object(level_object)

        if isinstance(level_object, LevelObject):
            self.level_view.create_object_at(*pos, level_object.domain,
                                             level_object.obj_index)
        elif isinstance(level_object, EnemyObject):
            self.level_view.add_enemy(level_object.obj_index, *pos, -1)

    def on_about(self, _):
        about = AboutDialog(self)

        about.show()

    def closeEvent(self, event: QCloseEvent):
        if not self.safe_to_change():
            event.ignore()

            return

        super(MainWindow, self).closeEvent(event)
コード例 #5
0
class FittingResultViewer(QDialog):
    PAGE_ROWS = 20
    logger = logging.getLogger("root.QGrain.ui.FittingResultViewer")
    result_marked = Signal(SSUResult)

    def __init__(self, reference_viewer: ReferenceResultViewer, parent=None):
        super().__init__(parent=parent, f=Qt.Window)
        self.setWindowTitle(self.tr("SSU Fitting Result Viewer"))
        self.__fitting_results = []  # type: list[SSUResult]
        self.retry_tasks = {}  # type: dict[UUID, SSUTask]
        self.__reference_viewer = reference_viewer
        self.init_ui()
        self.boxplot_chart = BoxplotChart(parent=self, toolbar=True)
        self.typical_chart = SSUTypicalComponentChart(parent=self,
                                                      toolbar=True)
        self.distance_chart = DistanceCurveChart(parent=self, toolbar=True)
        self.mixed_distribution_chart = MixedDistributionChart(
            parent=self, toolbar=True, use_animation=True)
        self.file_dialog = QFileDialog(parent=self)
        self.async_worker = AsyncWorker()
        self.async_worker.background_worker.task_succeeded.connect(
            self.on_fitting_succeeded)
        self.async_worker.background_worker.task_failed.connect(
            self.on_fitting_failed)
        self.update_page_list()
        self.update_page(self.page_index)

        self.normal_msg = QMessageBox(self)
        self.remove_warning_msg = QMessageBox(self)
        self.remove_warning_msg.setStandardButtons(QMessageBox.No
                                                   | QMessageBox.Yes)
        self.remove_warning_msg.setDefaultButton(QMessageBox.No)
        self.remove_warning_msg.setWindowTitle(self.tr("Warning"))
        self.remove_warning_msg.setText(
            self.tr("Are you sure to remove all SSU results?"))
        self.outlier_msg = QMessageBox(self)
        self.outlier_msg.setStandardButtons(QMessageBox.Discard
                                            | QMessageBox.Retry
                                            | QMessageBox.Ignore)
        self.outlier_msg.setDefaultButton(QMessageBox.Ignore)
        self.retry_progress_msg = QMessageBox()
        self.retry_progress_msg.addButton(QMessageBox.Ok)
        self.retry_progress_msg.button(QMessageBox.Ok).hide()
        self.retry_progress_msg.setWindowTitle(self.tr("Progress"))
        self.retry_timer = QTimer(self)
        self.retry_timer.setSingleShot(True)
        self.retry_timer.timeout.connect(
            lambda: self.retry_progress_msg.exec_())

    def init_ui(self):
        self.data_table = QTableWidget(100, 100)
        self.data_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.data_table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.data_table.setAlternatingRowColors(True)
        self.data_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.main_layout = QGridLayout(self)
        self.main_layout.addWidget(self.data_table, 0, 0, 1, 3)

        self.previous_button = QPushButton(
            qta.icon("mdi.skip-previous-circle"), self.tr("Previous"))
        self.previous_button.setToolTip(
            self.tr("Click to back to the previous page."))
        self.previous_button.clicked.connect(self.on_previous_button_clicked)
        self.current_page_combo_box = QComboBox()
        self.current_page_combo_box.addItem(self.tr("Page {0}").format(1))
        self.current_page_combo_box.currentIndexChanged.connect(
            self.update_page)
        self.next_button = QPushButton(qta.icon("mdi.skip-next-circle"),
                                       self.tr("Next"))
        self.next_button.setToolTip(self.tr("Click to jump to the next page."))
        self.next_button.clicked.connect(self.on_next_button_clicked)
        self.main_layout.addWidget(self.previous_button, 1, 0)
        self.main_layout.addWidget(self.current_page_combo_box, 1, 1)
        self.main_layout.addWidget(self.next_button, 1, 2)

        self.distance_label = QLabel(self.tr("Distance"))
        self.distance_label.setToolTip(
            self.
            tr("It's the function to calculate the difference (on the contrary, similarity) between two samples."
               ))
        self.distance_combo_box = QComboBox()
        self.distance_combo_box.addItems(built_in_distances)
        self.distance_combo_box.setCurrentText("log10MSE")
        self.distance_combo_box.currentTextChanged.connect(
            lambda: self.update_page(self.page_index))
        self.main_layout.addWidget(self.distance_label, 2, 0)
        self.main_layout.addWidget(self.distance_combo_box, 2, 1, 1, 2)
        self.menu = QMenu(self.data_table)
        self.menu.setShortcutAutoRepeat(True)
        self.mark_action = self.menu.addAction(
            qta.icon("mdi.marker-check"),
            self.tr("Mark Selection(s) as Reference"))
        self.mark_action.triggered.connect(self.mark_selections)
        self.remove_selection_action = self.menu.addAction(
            qta.icon("fa.remove"), self.tr("Remove Selection(s)"))
        self.remove_selection_action.triggered.connect(self.remove_selections)
        self.remove_all_action = self.menu.addAction(qta.icon("fa.remove"),
                                                     self.tr("Remove All"))
        self.remove_all_action.triggered.connect(self.remove_all_results)
        self.plot_loss_chart_action = self.menu.addAction(
            qta.icon("mdi.chart-timeline-variant"), self.tr("Plot Loss Chart"))
        self.plot_loss_chart_action.triggered.connect(self.show_distance)
        self.plot_distribution_chart_action = self.menu.addAction(
            qta.icon("fa5s.chart-area"), self.tr("Plot Distribution Chart"))
        self.plot_distribution_chart_action.triggered.connect(
            self.show_distribution)
        self.plot_distribution_animation_action = self.menu.addAction(
            qta.icon("fa5s.chart-area"),
            self.tr("Plot Distribution Chart (Animation)"))
        self.plot_distribution_animation_action.triggered.connect(
            self.show_history_distribution)

        self.detect_outliers_menu = self.menu.addMenu(
            qta.icon("mdi.magnify"), self.tr("Detect Outliers"))
        self.check_nan_and_inf_action = self.detect_outliers_menu.addAction(
            self.tr("Check NaN and Inf"))
        self.check_nan_and_inf_action.triggered.connect(self.check_nan_and_inf)
        self.check_final_distances_action = self.detect_outliers_menu.addAction(
            self.tr("Check Final Distances"))
        self.check_final_distances_action.triggered.connect(
            self.check_final_distances)
        self.check_component_mean_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Mean"))
        self.check_component_mean_action.triggered.connect(
            lambda: self.check_component_moments("mean"))
        self.check_component_std_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component STD"))
        self.check_component_std_action.triggered.connect(
            lambda: self.check_component_moments("std"))
        self.check_component_skewness_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Skewness"))
        self.check_component_skewness_action.triggered.connect(
            lambda: self.check_component_moments("skewness"))
        self.check_component_kurtosis_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Kurtosis"))
        self.check_component_kurtosis_action.triggered.connect(
            lambda: self.check_component_moments("kurtosis"))
        self.check_component_fractions_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Fractions"))
        self.check_component_fractions_action.triggered.connect(
            self.check_component_fractions)
        self.degrade_results_action = self.detect_outliers_menu.addAction(
            self.tr("Degrade Results"))
        self.degrade_results_action.triggered.connect(self.degrade_results)
        self.try_align_components_action = self.detect_outliers_menu.addAction(
            self.tr("Try Align Components"))
        self.try_align_components_action.triggered.connect(
            self.try_align_components)

        self.analyse_typical_components_action = self.menu.addAction(
            qta.icon("ei.tags"), self.tr("Analyse Typical Components"))
        self.analyse_typical_components_action.triggered.connect(
            self.analyse_typical_components)
        self.load_dump_action = self.menu.addAction(
            qta.icon("fa.database"), self.tr("Load Binary Dump"))
        self.load_dump_action.triggered.connect(self.load_dump)
        self.save_dump_action = self.menu.addAction(
            qta.icon("fa.save"), self.tr("Save Binary Dump"))
        self.save_dump_action.triggered.connect(self.save_dump)
        self.save_excel_action = self.menu.addAction(
            qta.icon("mdi.microsoft-excel"), self.tr("Save Excel"))
        self.save_excel_action.triggered.connect(
            lambda: self.on_save_excel_clicked(align_components=False))
        self.save_excel_align_action = self.menu.addAction(
            qta.icon("mdi.microsoft-excel"),
            self.tr("Save Excel (Force Alignment)"))
        self.save_excel_align_action.triggered.connect(
            lambda: self.on_save_excel_clicked(align_components=True))
        self.data_table.customContextMenuRequested.connect(self.show_menu)
        # necessary to add actions of menu to this widget itself,
        # otherwise, the shortcuts will not be triggered
        self.addActions(self.menu.actions())

    def show_menu(self, pos: QPoint):
        self.menu.popup(QCursor.pos())

    def show_message(self, title: str, message: str):
        self.normal_msg.setWindowTitle(title)
        self.normal_msg.setText(message)
        self.normal_msg.exec_()

    def show_info(self, message: str):
        self.show_message(self.tr("Info"), message)

    def show_warning(self, message: str):
        self.show_message(self.tr("Warning"), message)

    def show_error(self, message: str):
        self.show_message(self.tr("Error"), message)

    @property
    def distance_name(self) -> str:
        return self.distance_combo_box.currentText()

    @property
    def distance_func(self) -> typing.Callable:
        return get_distance_func_by_name(self.distance_combo_box.currentText())

    @property
    def page_index(self) -> int:
        return self.current_page_combo_box.currentIndex()

    @property
    def n_pages(self) -> int:
        return self.current_page_combo_box.count()

    @property
    def n_results(self) -> int:
        return len(self.__fitting_results)

    @property
    def selections(self):
        start = self.page_index * self.PAGE_ROWS
        temp = set()
        for item in self.data_table.selectedRanges():
            for i in range(item.topRow(),
                           min(self.PAGE_ROWS + 1,
                               item.bottomRow() + 1)):
                temp.add(i + start)
        indexes = list(temp)
        indexes.sort()
        return indexes

    def update_page_list(self):
        last_page_index = self.page_index
        if self.n_results == 0:
            n_pages = 1
        else:
            n_pages, left = divmod(self.n_results, self.PAGE_ROWS)
            if left != 0:
                n_pages += 1
        self.current_page_combo_box.blockSignals(True)
        self.current_page_combo_box.clear()
        self.current_page_combo_box.addItems(
            [self.tr("Page {0}").format(i + 1) for i in range(n_pages)])
        if last_page_index >= n_pages:
            self.current_page_combo_box.setCurrentIndex(n_pages - 1)
        else:
            self.current_page_combo_box.setCurrentIndex(last_page_index)
        self.current_page_combo_box.blockSignals(False)

    def update_page(self, page_index: int):
        def write(row: int, col: int, value: str):
            if isinstance(value, str):
                pass
            elif isinstance(value, int):
                value = str(value)
            elif isinstance(value, float):
                value = f"{value: 0.4f}"
            else:
                value = value.__str__()
            item = QTableWidgetItem(value)
            item.setTextAlignment(Qt.AlignCenter)
            self.data_table.setItem(row, col, item)

        # necessary to clear
        self.data_table.clear()
        if page_index == self.n_pages - 1:
            start = page_index * self.PAGE_ROWS
            end = self.n_results
        else:
            start, end = page_index * self.PAGE_ROWS, (page_index +
                                                       1) * self.PAGE_ROWS
        self.data_table.setRowCount(end - start)
        self.data_table.setColumnCount(7)
        self.data_table.setHorizontalHeaderLabels([
            self.tr("Resolver"),
            self.tr("Distribution Type"),
            self.tr("N_components"),
            self.tr("N_iterations"),
            self.tr("Spent Time [s]"),
            self.tr("Final Distance"),
            self.tr("Has Reference")
        ])
        sample_names = [
            result.sample.name for result in self.__fitting_results[start:end]
        ]
        self.data_table.setVerticalHeaderLabels(sample_names)
        for row, result in enumerate(self.__fitting_results[start:end]):
            write(row, 0, result.task.resolver)
            write(row, 1,
                  self.get_distribution_name(result.task.distribution_type))
            write(row, 2, result.task.n_components)
            write(row, 3, result.n_iterations)
            write(row, 4, result.time_spent)
            write(
                row, 5,
                self.distance_func(result.sample.distribution,
                                   result.distribution))
            has_ref = result.task.initial_guess is not None or result.task.reference is not None
            write(row, 6, self.tr("Yes") if has_ref else self.tr("No"))

        self.data_table.resizeColumnsToContents()

    def on_previous_button_clicked(self):
        if self.page_index > 0:
            self.current_page_combo_box.setCurrentIndex(self.page_index - 1)

    def on_next_button_clicked(self):
        if self.page_index < self.n_pages - 1:
            self.current_page_combo_box.setCurrentIndex(self.page_index + 1)

    def get_distribution_name(self, distribution_type: DistributionType):
        if distribution_type == DistributionType.Normal:
            return self.tr("Normal")
        elif distribution_type == DistributionType.Weibull:
            return self.tr("Weibull")
        elif distribution_type == DistributionType.SkewNormal:
            return self.tr("Skew Normal")
        else:
            raise NotImplementedError(distribution_type)

    def add_result(self, result: SSUResult):
        if self.n_results == 0 or \
            (self.page_index == self.n_pages - 1 and \
            divmod(self.n_results, self.PAGE_ROWS)[-1] != 0):
            need_update = True
        else:
            need_update = False
        self.__fitting_results.append(result)
        self.update_page_list()
        if need_update:
            self.update_page(self.page_index)

    def add_results(self, results: typing.List[SSUResult]):
        if self.n_results == 0 or \
            (self.page_index == self.n_pages - 1 and \
            divmod(self.n_results, self.PAGE_ROWS)[-1] != 0):
            need_update = True
        else:
            need_update = False
        self.__fitting_results.extend(results)
        self.update_page_list()
        if need_update:
            self.update_page(self.page_index)

    def mark_selections(self):
        for index in self.selections:
            self.result_marked.emit(self.__fitting_results[index])

    def remove_results(self, indexes):
        results = []
        for i in reversed(indexes):
            res = self.__fitting_results.pop(i)
            results.append(res)
        self.update_page_list()
        self.update_page(self.page_index)

    def remove_selections(self):
        indexes = self.selections
        self.remove_results(indexes)

    def remove_all_results(self):
        res = self.remove_warning_msg.exec_()
        if res == QMessageBox.Yes:
            self.__fitting_results.clear()
            self.update_page_list()
            self.update_page(0)

    def show_distance(self):
        results = [self.__fitting_results[i] for i in self.selections]
        if results is None or len(results) == 0:
            return
        result = results[0]
        self.distance_chart.show_distance_series(result.get_distance_series(
            self.distance_name),
                                                 title=result.sample.name)
        self.distance_chart.show()

    def show_distribution(self):
        results = [self.__fitting_results[i] for i in self.selections]
        if results is None or len(results) == 0:
            return
        result = results[0]
        self.mixed_distribution_chart.show_model(result.view_model)
        self.mixed_distribution_chart.show()

    def show_history_distribution(self):
        results = [self.__fitting_results[i] for i in self.selections]
        if results is None or len(results) == 0:
            return
        result = results[0]
        self.mixed_distribution_chart.show_result(result)
        self.mixed_distribution_chart.show()

    def load_dump(self):
        filename, _ = self.file_dialog.getOpenFileName(
            self, self.tr("Select a binary dump file of SSU results"), None,
            self.tr("Binary dump (*.dump)"))
        if filename is None or filename == "":
            return
        with open(filename, "rb") as f:
            results = pickle.load(f)  # type: list[SSUResult]
            valid = True
            if isinstance(results, list):
                for result in results:
                    if not isinstance(result, SSUResult):
                        valid = False
                        break
            else:
                valid = False

            if valid:
                if self.n_results != 0 and len(results) != 0:
                    old_classes = self.__fitting_results[0].classes_φ
                    new_classes = results[0].classes_φ
                    classes_inconsistent = False
                    if len(old_classes) != len(new_classes):
                        classes_inconsistent = True
                    else:
                        classes_error = np.abs(old_classes - new_classes)
                        if not np.all(np.less_equal(classes_error, 1e-8)):
                            classes_inconsistent = True
                    if classes_inconsistent:
                        self.show_error(
                            self.
                            tr("The results in the dump file has inconsistent grain-size classes with that in your list."
                               ))
                        return
                self.add_results(results)
            else:
                self.show_error(self.tr("The binary dump file is invalid."))

    def save_dump(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        filename, _ = self.file_dialog.getSaveFileName(
            self, self.tr("Save the SSU results to binary dump file"), None,
            self.tr("Binary dump (*.dump)"))
        if filename is None or filename == "":
            return
        with open(filename, "wb") as f:
            pickle.dump(self.__fitting_results, f)

    def save_excel(self, filename, align_components=False):
        if self.n_results == 0:
            return

        results = self.__fitting_results.copy()
        classes_μm = results[0].classes_μm
        n_components_list = [
            result.n_components for result in self.__fitting_results
        ]
        count_dict = Counter(n_components_list)
        max_n_components = max(count_dict.keys())
        self.logger.debug(
            f"N_components: {count_dict}, Max N_components: {max_n_components}"
        )

        flags = []
        if not align_components:
            for result in results:
                flags.extend(range(result.n_components))
        else:
            n_components_desc = "\n".join([
                self.tr("{0} Component(s): {1}").format(n_components, count)
                for n_components, count in count_dict.items()
            ])
            self.show_info(
                self.tr("N_components distribution of Results:\n{0}").format(
                    n_components_desc))
            stacked_components = []
            for result in self.__fitting_results:
                for component in result.components:
                    stacked_components.append(component.distribution)
            stacked_components = np.array(stacked_components)
            cluser = KMeans(n_clusters=max_n_components)
            flags = cluser.fit_predict(stacked_components)
            # check flags to make it unique
            flag_index = 0
            for i, result in enumerate(self.__fitting_results):
                result_flags = set()
                for component in result.components:
                    if flags[flag_index] in result_flags:
                        if flags[flag_index] == max_n_components:
                            flags[flag_index] = max_n_components - 1
                        else:
                            flag_index[flag_index] += 1
                        result_flags.add(flags[flag_index])
                    flag_index += 1

            flag_set = set(flags)
            picked = []
            for target_flag in flag_set:
                for i, flag in enumerate(flags):
                    if flag == target_flag:
                        picked.append(
                            (target_flag,
                             logarithmic(classes_μm,
                                         stacked_components[i])["mean"]))
                        break
            picked.sort(key=lambda x: x[1])
            flag_map = {flag: index for index, (flag, _) in enumerate(picked)}
            flags = np.array([flag_map[flag] for flag in flags])

        wb = openpyxl.Workbook()
        prepare_styles(wb)
        ws = wb.active
        ws.title = self.tr("README")
        description = \
            """
            This Excel file was generated by QGrain ({0}).

            Please cite:
            Liu, Y., Liu, X., Sun, Y., 2021. QGrain: An open-source and easy-to-use software for the comprehensive analysis of grain size distributions. Sedimentary Geology 423, 105980. https://doi.org/10.1016/j.sedgeo.2021.105980

            It contanins 4 + max(N_components) sheets:
            1. The first sheet is the sample distributions of SSU results.
            2. The second sheet is used to put the infomation of fitting.
            3. The third sheet is the statistic parameters calculated by statistic moment method.
            4. The fouth sheet is the distributions of unmixed components and their sum of each sample.
            5. Other sheets are the unmixed end-member distributions which were discretely stored.

            The SSU algorithm is implemented by QGrain.

            """.format(QGRAIN_VERSION)

        def write(row, col, value, style="normal_light"):
            cell = ws.cell(row + 1, col + 1, value=value)
            cell.style = style

        lines_of_desc = description.split("\n")
        for row, line in enumerate(lines_of_desc):
            write(row, 0, line, style="description")
        ws.column_dimensions[column_to_char(0)].width = 200

        ws = wb.create_sheet(self.tr("Sample Distributions"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for col, value in enumerate(classes_μm, 1):
            write(0, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        for row, result in enumerate(results, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, result.sample.name, style=style)
            for col, value in enumerate(result.sample.distribution, 1):
                write(row, col, value, style=style)
            QCoreApplication.processEvents()

        ws = wb.create_sheet(self.tr("Information of Fitting"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        headers = [
            self.tr("Distribution Type"),
            self.tr("N_components"),
            self.tr("Resolver"),
            self.tr("Resolver Settings"),
            self.tr("Initial Guess"),
            self.tr("Reference"),
            self.tr("Spent Time [s]"),
            self.tr("N_iterations"),
            self.tr("Final Distance [log10MSE]")
        ]
        for col, value in enumerate(headers, 1):
            write(0, col, value, style="header")
            if col in (4, 5, 6):
                ws.column_dimensions[column_to_char(col)].width = 10
            else:
                ws.column_dimensions[column_to_char(col)].width = 10
        for row, result in enumerate(results, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, result.sample.name, style=style)
            write(row, 1, result.distribution_type.name, style=style)
            write(row, 2, result.n_components, style=style)
            write(row, 3, result.task.resolver, style=style)
            write(row,
                  4,
                  self.tr("Default") if result.task.resolver_setting is None
                  else result.task.resolver_setting.__str__(),
                  style=style)
            write(row,
                  5,
                  self.tr("None") if result.task.initial_guess is None else
                  result.task.initial_guess.__str__(),
                  style=style)
            write(row,
                  6,
                  self.tr("None") if result.task.reference is None else
                  result.task.reference.__str__(),
                  style=style)
            write(row, 7, result.time_spent, style=style)
            write(row, 8, result.n_iterations, style=style)
            write(row, 9, result.get_distance("log10MSE"), style=style)

        ws = wb.create_sheet(self.tr("Statistic Moments"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=1)
        ws.column_dimensions[column_to_char(0)].width = 16
        headers = []
        sub_headers = [
            self.tr("Proportion"),
            self.tr("Mean [φ]"),
            self.tr("Mean [μm]"),
            self.tr("STD [φ]"),
            self.tr("STD [μm]"),
            self.tr("Skewness"),
            self.tr("Kurtosis")
        ]
        for i in range(max_n_components):
            write(0,
                  i * len(sub_headers) + 1,
                  self.tr("C{0}").format(i + 1),
                  style="header")
            ws.merge_cells(start_row=1,
                           start_column=i * len(sub_headers) + 2,
                           end_row=1,
                           end_column=(i + 1) * len(sub_headers) + 1)
            headers.extend(sub_headers)
        for col, value in enumerate(headers, 1):
            write(1, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        flag_index = 0
        for row, result in enumerate(results, 2):
            if row % 2 == 0:
                style = "normal_light"
            else:
                style = "normal_dark"
            write(row, 0, result.sample.name, style=style)
            for component in result.components:
                index = flags[flag_index]
                write(row,
                      index * len(sub_headers) + 1,
                      component.fraction,
                      style=style)
                write(row,
                      index * len(sub_headers) + 2,
                      component.logarithmic_moments["mean"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 3,
                      component.geometric_moments["mean"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 4,
                      component.logarithmic_moments["std"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 5,
                      component.geometric_moments["std"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 6,
                      component.logarithmic_moments["skewness"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 7,
                      component.logarithmic_moments["kurtosis"],
                      style=style)
                flag_index += 1

        ws = wb.create_sheet(self.tr("Unmixed Components"))
        ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=2)
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for col, value in enumerate(classes_μm, 2):
            write(0, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        row = 1
        for result_index, result in enumerate(results, 1):
            if result_index % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, result.sample.name, style=style)
            ws.merge_cells(start_row=row + 1,
                           start_column=1,
                           end_row=row + result.n_components + 1,
                           end_column=1)
            for component_i, component in enumerate(result.components, 1):
                write(row, 1, self.tr("C{0}").format(component_i), style=style)
                for col, value in enumerate(
                        component.distribution * component.fraction, 2):
                    write(row, col, value, style=style)
                row += 1
            write(row, 1, self.tr("Sum"), style=style)
            for col, value in enumerate(result.distribution, 2):
                write(row, col, value, style=style)
            row += 1

        ws_dict = {}
        flag_set = set(flags)
        for flag in flag_set:
            ws = wb.create_sheet(self.tr("Unmixed EM{0}").format(flag + 1))
            write(0, 0, self.tr("Sample Name"), style="header")
            ws.column_dimensions[column_to_char(0)].width = 16
            for col, value in enumerate(classes_μm, 1):
                write(0, col, value, style="header")
                ws.column_dimensions[column_to_char(col)].width = 10
            ws_dict[flag] = ws

        flag_index = 0
        for row, result in enumerate(results, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"

            for component in result.components:
                flag = flags[flag_index]
                ws = ws_dict[flag]
                write(row, 0, result.sample.name, style=style)
                for col, value in enumerate(component.distribution, 1):
                    write(row, col, value, style=style)
                flag_index += 1

        wb.save(filename)
        wb.close()

    def on_save_excel_clicked(self, align_components=False):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any SSU result."))
            return
        filename, _ = self.file_dialog.getSaveFileName(
            None, self.tr("Choose a filename to save SSU Results"), None,
            "Microsoft Excel (*.xlsx)")
        if filename is None or filename == "":
            return
        try:
            self.save_excel(filename, align_components)
            self.show_info(
                self.tr("SSU results have been saved to:\n    {0}").format(
                    filename))
        except Exception as e:
            self.show_error(
                self.
                tr("Error raised while save SSU results to Excel file.\n    {0}"
                   ).format(e.__str__()))

    def on_fitting_succeeded(self, result: SSUResult):
        result_replace_index = self.retry_tasks[result.task.uuid]
        self.__fitting_results[result_replace_index] = result
        self.retry_tasks.pop(result.task.uuid)
        self.retry_progress_msg.setText(
            self.tr("Tasks to be retried: {0}").format(len(self.retry_tasks)))
        if len(self.retry_tasks) == 0:
            self.retry_progress_msg.close()

        self.logger.debug(
            f"Retried task succeeded, sample name={result.task.sample.name}, distribution_type={result.task.distribution_type.name}, n_components={result.task.n_components}"
        )
        self.update_page(self.page_index)

    def on_fitting_failed(self, failed_info: str, task: SSUTask):
        # necessary to remove it from the dict
        self.retry_tasks.pop(task.uuid)
        if len(self.retry_tasks) == 0:
            self.retry_progress_msg.close()
        self.show_error(
            self.tr("Failed to retry task, sample name={0}.\n{1}").format(
                task.sample.name, failed_info))
        self.logger.warning(
            f"Failed to retry task, sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}"
        )

    def retry_results(self, indexes, results):
        assert len(indexes) == len(results)
        if len(results) == 0:
            return
        self.retry_progress_msg.setText(
            self.tr("Tasks to be retried: {0}").format(len(results)))
        self.retry_timer.start(1)
        for index, result in zip(indexes, results):
            query = self.__reference_viewer.query_reference(result.sample)
            ref_result = None
            if query is None:
                nearby_results = self.__fitting_results[
                    index - 5:index] + self.__fitting_results[index + 1:index +
                                                              6]
                ref_result = self.__reference_viewer.find_similar(
                    result.sample, nearby_results)
            else:
                ref_result = query
            keys = ["mean", "std", "skewness"]
            # reference = [{key: comp.logarithmic_moments[key] for key in keys} for comp in ref_result.components]
            task = SSUTask(
                result.sample,
                ref_result.distribution_type,
                ref_result.n_components,
                resolver=ref_result.task.resolver,
                resolver_setting=ref_result.task.resolver_setting,
                #    reference=reference)
                initial_guess=ref_result.last_func_args)

            self.logger.debug(
                f"Retry task: sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}"
            )
            self.retry_tasks[task.uuid] = index
            self.async_worker.execute_task(task)

    def degrade_results(self):
        degrade_results = []  # type: list[SSUResult]
        degrade_indexes = []  # type: list[int]
        for i, result in enumerate(self.__fitting_results):
            for component in result.components:
                if component.fraction < 1e-3:
                    degrade_results.append(result)
                    degrade_indexes.append(i)
                    break
        self.logger.debug(
            f"Results should be degrade (have a redundant component): {[result.sample.name for result in degrade_results]}"
        )
        if len(degrade_results) == 0:
            self.show_info(
                self.tr("No fitting result was evaluated as an outlier."))
            return
        self.show_info(
            self.
            tr("The results below should be degrade (have a redundant component:\n    {0}"
               ).format(", ".join(
                   [result.sample.name for result in degrade_results])))

        self.retry_progress_msg.setText(
            self.tr("Tasks to be retried: {0}").format(len(degrade_results)))
        self.retry_timer.start(1)
        for index, result in zip(degrade_indexes, degrade_results):
            reference = []
            n_redundant = 0
            for component in result.components:
                if component.fraction < 1e-3:
                    n_redundant += 1
                else:
                    reference.append(
                        dict(mean=component.logarithmic_moments["mean"],
                             std=component.logarithmic_moments["std"],
                             skewness=component.logarithmic_moments["skewness"]
                             ))
            task = SSUTask(
                result.sample,
                result.distribution_type,
                result.n_components -
                n_redundant if result.n_components > n_redundant else 1,
                resolver=result.task.resolver,
                resolver_setting=result.task.resolver_setting,
                reference=reference)
            self.logger.debug(
                f"Retry task: sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}"
            )
            self.retry_tasks[task.uuid] = index
            self.async_worker.execute_task(task)

    def ask_deal_outliers(self, outlier_results: typing.List[SSUResult],
                          outlier_indexes: typing.List[int]):
        assert len(outlier_indexes) == len(outlier_results)
        if len(outlier_results) == 0:
            self.show_info(
                self.tr("No fitting result was evaluated as an outlier."))
        else:
            if len(outlier_results) > 100:
                self.outlier_msg.setText(
                    self.
                    tr("The fitting results have the component that its fraction is near zero:\n    {0}...(total {1} outliers)\nHow to deal with them?"
                       ).format(
                           ", ".join([
                               result.sample.name
                               for result in outlier_results[:100]
                           ]), len(outlier_results)))
            else:
                self.outlier_msg.setText(
                    self.
                    tr("The fitting results have the component that its fraction is near zero:\n    {0}\nHow to deal with them?"
                       ).format(", ".join([
                           result.sample.name for result in outlier_results
                       ])))
            res = self.outlier_msg.exec_()
            if res == QMessageBox.Discard:
                self.remove_results(outlier_indexes)
            elif res == QMessageBox.Retry:
                self.retry_results(outlier_indexes, outlier_results)
            else:
                pass

    def check_nan_and_inf(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        outlier_results = []
        outlier_indexes = []
        for i, result in enumerate(self.__fitting_results):
            if not result.is_valid:
                outlier_results.append(result)
                outlier_indexes.append(i)
        self.logger.debug(
            f"Outlier results with the nan or inf value(s): {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def check_final_distances(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return
        distances = []
        for result in self.__fitting_results:
            distances.append(result.get_distance(self.distance_name))
        distances = np.array(distances)
        self.boxplot_chart.show_dataset([distances],
                                        xlabels=[self.distance_name],
                                        ylabel=self.tr("Distance"))
        self.boxplot_chart.show()

        # calculate the 1/4, 1/2, and 3/4 postion value to judge which result is invalid
        # 1. the mean squared errors are much higher in the results which are lack of components
        # 2. with the component number getting higher, the mean squared error will get lower and finally reach the minimum
        median = np.median(distances)
        upper_group = distances[np.greater(distances, median)]
        lower_group = distances[np.less(distances, median)]
        value_1_4 = np.median(lower_group)
        value_3_4 = np.median(upper_group)
        distance_QR = value_3_4 - value_1_4
        outlier_results = []
        outlier_indexes = []
        for i, (result,
                distance) in enumerate(zip(self.__fitting_results, distances)):
            if distance > value_3_4 + distance_QR * 1.5:
                # which error too small is not outlier
                # if distance > value_3_4 + distance_QR * 1.5 or distance < value_1_4 - distance_QR * 1.5:
                outlier_results.append(result)
                outlier_indexes.append(i)
        self.logger.debug(
            f"Outlier results with too greater distances: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def check_component_moments(self, key: str):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return
        max_n_components = 0
        for result in self.__fitting_results:
            if result.n_components > max_n_components:
                max_n_components = result.n_components
        moments = []
        for i in range(max_n_components):
            moments.append([])

        for result in self.__fitting_results:
            for i, component in enumerate(result.components):
                if np.isnan(component.logarithmic_moments[key]) or np.isinf(
                        component.logarithmic_moments[key]):
                    pass
                else:
                    moments[i].append(component.logarithmic_moments[key])

        # key_trans = {"mean": self.tr("Mean"), "std": self.tr("STD"), "skewness": self.tr("Skewness"), "kurtosis": self.tr("Kurtosis")}
        key_label_trans = {
            "mean": self.tr("Mean [φ]"),
            "std": self.tr("STD [φ]"),
            "skewness": self.tr("Skewness"),
            "kurtosis": self.tr("Kurtosis")
        }
        self.boxplot_chart.show_dataset(
            moments,
            xlabels=[f"C{i+1}" for i in range(max_n_components)],
            ylabel=key_label_trans[key])
        self.boxplot_chart.show()

        outlier_dict = {}

        for i in range(max_n_components):
            stacked_moments = np.array(moments[i])
            # calculate the 1/4, 1/2, and 3/4 postion value to judge which result is invalid
            # 1. the mean squared errors are much higher in the results which are lack of components
            # 2. with the component number getting higher, the mean squared error will get lower and finally reach the minimum
            median = np.median(stacked_moments)
            upper_group = stacked_moments[np.greater(stacked_moments, median)]
            lower_group = stacked_moments[np.less(stacked_moments, median)]
            value_1_4 = np.median(lower_group)
            value_3_4 = np.median(upper_group)
            distance_QR = value_3_4 - value_1_4

            for j, result in enumerate(self.__fitting_results):
                if result.n_components > i:
                    distance = result.components[i].logarithmic_moments[key]
                    if distance > value_3_4 + distance_QR * 1.5 or distance < value_1_4 - distance_QR * 1.5:
                        outlier_dict[j] = result

        outlier_results = []
        outlier_indexes = []
        for index, result in sorted(outlier_dict.items(), key=lambda x: x[0]):
            outlier_indexes.append(index)
            outlier_results.append(result)
        self.logger.debug(
            f"Outlier results with abnormal {key} values of their components: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def check_component_fractions(self):
        outlier_results = []
        outlier_indexes = []
        for i, result in enumerate(self.__fitting_results):
            for component in result.components:
                if component.fraction < 1e-3:
                    outlier_results.append(result)
                    outlier_indexes.append(i)
                    break
        self.logger.debug(
            f"Outlier results with the component that its fraction is near zero: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def try_align_components(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return
        import matplotlib.pyplot as plt
        n_components_list = [
            result.n_components for result in self.__fitting_results
        ]
        count_dict = Counter(n_components_list)
        max_n_components = max(count_dict.keys())
        self.logger.debug(
            f"N_components: {count_dict}, Max N_components: {max_n_components}"
        )
        n_components_desc = "\n".join([
            self.tr("{0} Component(s): {1}").format(n_components, count)
            for n_components, count in count_dict.items()
        ])
        self.show_info(
            self.tr("N_components distribution of Results:\n{0}").format(
                n_components_desc))

        x = self.__fitting_results[0].classes_μm
        stacked_components = []
        for result in self.__fitting_results:
            for component in result.components:
                stacked_components.append(component.distribution)
        stacked_components = np.array(stacked_components)

        cluser = KMeans(n_clusters=max_n_components)
        flags = cluser.fit_predict(stacked_components)

        figure = plt.figure(figsize=(6, 4))
        cmap = plt.get_cmap("tab10")
        axes = figure.add_subplot(1, 1, 1)
        for flag, distribution in zip(flags, stacked_components):
            plt.plot(x, distribution, c=cmap(flag), zorder=flag)
        axes.set_xscale("log")
        axes.set_xlabel(self.tr("Grain-size [μm]"))
        axes.set_ylabel(self.tr("Frequency"))
        figure.tight_layout()
        figure.show()

        outlier_results = []
        outlier_indexes = []
        flag_index = 0
        for i, result in enumerate(self.__fitting_results):
            result_flags = set()
            for component in result.components:
                if flags[flag_index] in result_flags:
                    outlier_results.append(result)
                    outlier_indexes.append(i)
                    break
                else:
                    result_flags.add(flags[flag_index])
                flag_index += 1
        self.logger.debug(
            f"Outlier results that have two components in the same cluster: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def analyse_typical_components(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return

        self.typical_chart.show_typical(self.__fitting_results)
        self.typical_chart.show()