Пример #1
0
def _create_resource_type_combo(
        current_resource_type: ResourceType, parent: QWidget,
        resource_database: ResourceDatabase) -> QComboBox:
    """

    :param current_resource_type:
    :param parent:
    :return:
    """
    resource_type_combo = QComboBox(parent)

    for resource_type in iterate_enum(ResourceType):
        try:
            count_elements = len(resource_database.get_by_type(resource_type))
        except ValueError:
            count_elements = 0

        if count_elements == 0:
            continue

        resource_type_combo.addItem(resource_type.name, resource_type)
        if resource_type is current_resource_type:
            resource_type_combo.setCurrentIndex(resource_type_combo.count() -
                                                1)

    return resource_type_combo
Пример #2
0
    def set_editor_data(editor: QComboBox, index):
        current_value = index.model().data(index, Qt.EditRole)
        current_index = 0

        for idx in range(0, editor.count()):
            value = editor.itemText(idx)

            if value == current_value:
                current_index = idx

        editor.setCurrentIndex(current_index)
Пример #3
0
    def generateDataTypesField(dataType=None):
        """
		@rtype: QComboBox
		"""
        dataTypes = QComboBox()
        dataTypes.addItems(
            ['TINYINT', 'SMALLINT', 'MEDIUMINT', 'INT', 'BIGINT', 'BIT'])
        dataTypes.insertSeparator(dataTypes.count())
        dataTypes.addItems(['FLOAT', 'DOUBLE', 'DECIMAL'])
        dataTypes.insertSeparator(dataTypes.count())
        dataTypes.addItems(
            ['CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT'])
        dataTypes.insertSeparator(dataTypes.count())
        dataTypes.addItems([
            'BINARY', 'VARBINARY', 'TINYBLOB', 'BLOB', 'MEDIUMBLOB', 'LONGBLOB'
        ])
        dataTypes.insertSeparator(dataTypes.count())
        dataTypes.addItems(['DATE', 'TIME', 'YEAR', 'DATETIME', 'TIMESTAMP'])
        dataTypes.insertSeparator(dataTypes.count())
        dataTypes.addItems([
            'POINT', 'LINESTRING', 'POLYGON', 'GEOMETRY', 'MULTIPOINT',
            'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION'
        ])
        dataTypes.insertSeparator(dataTypes.count())
        dataTypes.addItems(['ENUM', 'SET'])

        if dataType is not None:
            dataTypes.setCurrentIndex(dataTypes.findText(dataType.upper()))

        return dataTypes
Пример #4
0
class DataTabel(QWidget):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)
        self.combo_file = QComboBox()
        label1 = QLabel("Input:")
        self.data_models = {}
        self.table_view = QTableView()

        layout1 = QHBoxLayout()
        layout1.addWidget(label1)
        layout1.addWidget(self.combo_file)
        layout1.setStretch(1, 1)

        self.layout2 = QVBoxLayout()
        self.layout2.addLayout(layout1)
        self.layout2.addWidget(self.table_view)

        self.setLayout(self.layout2)
        self.combo_file.currentIndexChanged.connect(self.update_model)

    def initialize(self, data_sets):
        self.combo_file.clear()
        for (file_name, data_frame) in data_sets.items():
            self.combo_file.addItem(file_name)
            model = DataModel(data_frame)
            self.data_models[file_name] = model
        if (self.combo_file.count() != 0):
            self.combo_file.setCurrentIndex(self.combo_file.count() - 1)
        self.update_model()

    def update_model(self):
        if (self.combo_file.currentText() in self.data_models):
            self.table_view.setModel(
                self.data_models[self.combo_file.currentText()])
        else:
            self.table_view.setModel(DataModel())

    def current_file(self):
        return self.combo_file.currentText()
Пример #5
0
def _create_resource_type_combo(current_resource_type: ResourceType, parent: QWidget) -> QComboBox:
    """

    :param current_resource_type:
    :param parent:
    :return:
    """
    resource_type_combo = QComboBox(parent)

    for resource_type in ResourceType:
        if not resource_type.is_usable_for_requirement:
            continue

        resource_type_combo.addItem(resource_type.name, resource_type)
        if resource_type is current_resource_type:
            resource_type_combo.setCurrentIndex(resource_type_combo.count() - 1)

    return resource_type_combo
Пример #6
0
    def create_editor(self, parent, option, index):
        editor = QComboBox(parent)
        editor.setEditable(True)
        editor.setValidator(self.validator)
        current_value = index.model().data(index, Qt.EditRole)
        current_index = 0

        for idx, value in enumerate(self.resolution_values):
            if current_value == value:
                current_index = idx
            editor.addItem(value)

        editor.setCurrentIndex(current_index)

        if current_value not in self.resolution_values:
            editor.addItem(current_value)
            editor.setCurrentIndex(editor.count())

        return editor
Пример #7
0
def _create_resource_type_combo(current_resource_type: ResourceType,
                                parent: QWidget) -> QComboBox:
    """

    :param current_resource_type:
    :param parent:
    :return:
    """
    resource_type_combo = QComboBox(parent)

    for resource_type in ResourceType:
        if resource_type == ResourceType.PICKUP_INDEX:
            continue

        resource_type_combo.addItem(resource_type.name, resource_type)
        if resource_type is current_resource_type:
            resource_type_combo.setCurrentIndex(resource_type_combo.count() -
                                                1)

    return resource_type_combo
Пример #8
0
def _create_resource_name_combo(resource_database: ResourceDatabase,
                                resource_type: ResourceType,
                                current_resource: Optional[ResourceInfo],
                                parent: QWidget,
                                ) -> QComboBox:
    """

    :param resource_database:
    :param current_resource:
    :param parent:
    :return:
    """

    resource_name_combo = QComboBox(parent)

    for resource in sorted(resource_database.get_by_type(resource_type), key=lambda x: x.long_name):
        resource_name_combo.addItem(resource.long_name, resource)
        if resource is current_resource:
            resource_name_combo.setCurrentIndex(resource_name_combo.count() - 1)

    return resource_name_combo
Пример #9
0
    def __init__(
        self,
        parent: QWidget,
        layout: QHBoxLayout,
        resource_database: ResourceDatabase,
        item: RequirementTemplate,
    ):
        self.parent = parent
        self.layout = layout
        self.resource_database = resource_database

        template_name_combo = QComboBox(parent)

        for template_name in sorted(
                resource_database.requirement_template.keys()):
            template_name_combo.addItem(template_name)
            if template_name == item.template_name:
                template_name_combo.setCurrentIndex(
                    template_name_combo.count() - 1)

        self.template_name_combo = template_name_combo
        self.layout.addWidget(self.template_name_combo)
Пример #10
0
class Window(QDialog):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)

        self.iconGroupBox = QGroupBox()
        self.iconLabel = QLabel()
        self.iconComboBox = QComboBox()
        self.showIconCheckBox = QCheckBox()

        self.messageGroupBox = QGroupBox()
        self.typeLabel = QLabel()
        self.durationLabel = QLabel()
        self.durationWarningLabel = QLabel()
        self.titleLabel = QLabel()
        self.bodyLabel = QLabel()

        self.typeComboBox = QComboBox()
        self.durationSpinBox = QSpinBox()
        self.titleEdit = QLineEdit()
        self.bodyEdit = QTextEdit()
        self.showMessageButton = QPushButton()

        self.minimizeAction = QAction()
        self.maximizeAction = QAction()
        self.restoreAction = QAction()
        self.quitAction = QAction()

        self.trayIcon = QSystemTrayIcon()
        self.trayIconMenu = QMenu()

        self.createIconGroupBox()
        self.createMessageGroupBox()

        self.iconLabel.setMinimumWidth(self.durationLabel.sizeHint().width())

        self.createActions()
        self.createTrayIcon()

        self.showMessageButton.clicked.connect(self.showMessage)
        self.showIconCheckBox.toggled.connect(self.trayIcon.setVisible)
        self.iconComboBox.currentIndexChanged.connect(self.setIcon)
        self.trayIcon.messageClicked.connect(self.messageClicked)
        self.trayIcon.activated.connect(self.iconActivated)

        self.mainLayout = QVBoxLayout()
        self.mainLayout.addWidget(self.iconGroupBox)
        self.mainLayout.addWidget(self.messageGroupBox)
        self.setLayout(self.mainLayout)

        self.iconComboBox.setCurrentIndex(1)
        self.trayIcon.show()

        self.setWindowTitle("Systray")
        self.resize(400, 300)

    def setVisible(self, visible):
        self.minimizeAction.setEnabled(visible)
        self.maximizeAction.setEnabled(not self.isMaximized())
        self.restoreAction.setEnabled(self.isMaximized() or not visible)
        super().setVisible(visible)

    def closeEvent(self, event):
        if not event.spontaneous() or not self.isVisible():
            return
        if self.trayIcon.isVisible():
            QMessageBox.information(self, "Systray",
                                    "The program will keep running in the system tray. "
                                    "To terminate the program, choose <b>Quit</b> in the context "
                                    "menu of the system tray entry.")
            self.hide()
            event.ignore()

    @Slot(int)
    def setIcon(self, index):
        icon = self.iconComboBox.itemIcon(index)
        self.trayIcon.setIcon(icon)
        self.setWindowIcon(icon)
        self.trayIcon.setToolTip(self.iconComboBox.itemText(index))

    @Slot(str)
    def iconActivated(self, reason):
        if reason == QSystemTrayIcon.Trigger:
            pass
        if reason == QSystemTrayIcon.DoubleClick:
            self.iconComboBox.setCurrentIndex(
                (self.iconComboBox.currentIndex() + 1) % self.iconComboBox.count()
            )
        if reason == QSystemTrayIcon.MiddleClick:
            self.showMessage()

    @Slot()
    def showMessage(self):
        self.showIconCheckBox.setChecked(True)
        selectedIcon = self.typeComboBox.itemData(self.typeComboBox.currentIndex())
        msgIcon = QSystemTrayIcon.MessageIcon(selectedIcon)

        if selectedIcon == -1:  # custom icon
            icon = QIcon(self.iconComboBox.itemIcon(self.iconComboBox.currentIndex()))
            self.trayIcon.showMessage(
                self.titleEdit.text(),
                self.bodyEdit.toPlainText(),
                icon,
                self.durationSpinBox.value() * 1000,
            )
        else:
            self.trayIcon.showMessage(
                self.titleEdit.text(),
                self.bodyEdit.toPlainText(),
                msgIcon,
                self.durationSpinBox.value() * 1000,
            )

    @Slot()
    def messageClicked(self):
        QMessageBox.information(None, "Systray",
                                "Sorry, I already gave what help I could.\n"
                                "Maybe you should try asking a human?")

    def createIconGroupBox(self):
        self.iconGroupBox = QGroupBox("Tray Icon")

        self.iconLabel = QLabel("Icon:")

        self.iconComboBox = QComboBox()
        self.iconComboBox.addItem(QIcon(":/images/bad.png"), "Bad")
        self.iconComboBox.addItem(QIcon(":/images/heart.png"), "Heart")
        self.iconComboBox.addItem(QIcon(":/images/trash.png"), "Trash")

        self.showIconCheckBox = QCheckBox("Show icon")
        self.showIconCheckBox.setChecked(True)

        iconLayout = QHBoxLayout()
        iconLayout.addWidget(self.iconLabel)
        iconLayout.addWidget(self.iconComboBox)
        iconLayout.addStretch()
        iconLayout.addWidget(self.showIconCheckBox)
        self.iconGroupBox.setLayout(iconLayout)

    def createMessageGroupBox(self):
        self.messageGroupBox = QGroupBox("Balloon Message")

        self.typeLabel = QLabel("Type:")

        self.typeComboBox = QComboBox()
        self.typeComboBox.addItem("None", QSystemTrayIcon.NoIcon)
        self.typeComboBox.addItem(
            self.style().standardIcon(QStyle.SP_MessageBoxInformation),
            "Information",
            QSystemTrayIcon.Information,
        )
        self.typeComboBox.addItem(
            self.style().standardIcon(QStyle.SP_MessageBoxWarning),
            "Warning",
            QSystemTrayIcon.Warning,
        )
        self.typeComboBox.addItem(
            self.style().standardIcon(QStyle.SP_MessageBoxCritical),
            "Critical",
            QSystemTrayIcon.Critical,
        )
        self.typeComboBox.addItem(QIcon(), "Custom icon", -1)
        self.typeComboBox.setCurrentIndex(1)

        self.durationLabel = QLabel("Duration:")

        self.durationSpinBox = QSpinBox()
        self.durationSpinBox.setRange(5, 60)
        self.durationSpinBox.setSuffix(" s")
        self.durationSpinBox.setValue(15)

        self.durationWarningLabel = QLabel("(some systems might ignore this hint)")
        self.durationWarningLabel.setIndent(10)

        self.titleLabel = QLabel("Title:")
        self.titleEdit = QLineEdit("Cannot connect to network")
        self.bodyLabel = QLabel("Body:")

        self.bodyEdit = QTextEdit()
        self.bodyEdit.setPlainText("Don't believe me. Honestly, I don't have a clue."
                                   "\nClick this balloon for details.")

        self.showMessageButton = QPushButton("Show Message")
        self.showMessageButton.setDefault(True)

        messageLayout = QGridLayout()
        messageLayout.addWidget(self.typeLabel, 0, 0)
        messageLayout.addWidget(self.typeComboBox, 0, 1, 1, 2)
        messageLayout.addWidget(self.durationLabel, 1, 0)
        messageLayout.addWidget(self.durationSpinBox, 1, 1)
        messageLayout.addWidget(self.durationWarningLabel, 1, 2, 1, 3)
        messageLayout.addWidget(self.titleLabel, 2, 0)
        messageLayout.addWidget(self.titleEdit, 2, 1, 1, 4)
        messageLayout.addWidget(self.bodyLabel, 3, 0)
        messageLayout.addWidget(self.bodyEdit, 3, 1, 2, 4)
        messageLayout.addWidget(self.showMessageButton, 5, 4)
        messageLayout.setColumnStretch(3, 1)
        messageLayout.setRowStretch(4, 1)
        self.messageGroupBox.setLayout(messageLayout)

    def createActions(self):
        self.minimizeAction = QAction("Minimize", self)
        self.minimizeAction.triggered.connect(self.hide)

        self.maximizeAction = QAction("Maximize", self)
        self.maximizeAction.triggered.connect(self.showMaximized)

        self.restoreAction = QAction("Restore", self)
        self.restoreAction.triggered.connect(self.showNormal)

        self.quitAction = QAction("Quit", self)
        self.quitAction.triggered.connect(qApp.quit)

    def createTrayIcon(self):
        self.trayIconMenu = QMenu(self)
        self.trayIconMenu.addAction(self.minimizeAction)
        self.trayIconMenu.addAction(self.maximizeAction)
        self.trayIconMenu.addAction(self.restoreAction)
        self.trayIconMenu.addSeparator()
        self.trayIconMenu.addAction(self.quitAction)

        self.trayIcon = QSystemTrayIcon(self)
        self.trayIcon.setContextMenu(self.trayIconMenu)
Пример #11
0
class CharUI(QWidget):
    """
    Main widget for the Character creator view.

    :param MainFrame mainframe: the application mainframe
    :param QWidget op: parent widget
    """
    def __init__(self, mainframe, op):
        print("Starting...")
        QWidget.__init__(self)
        self.mainframe = mainframe
        self.op = op
        self.nameT = None
        self.infoT = None
        self.importantB = None
        self.initUI()

    def initUI(self):
        """
        Initializes the GUI.
        Does a lot of stuff.
        """
        self.mainframe.setWindowTitle("Character Creator")

        grid = QGridLayout()
        self.setLayout(grid)

        nameL = QLabel(self, text="Name:")
        grid.addWidget(nameL, 1, 1)

        self.nameT = QLineEdit(self)
        self.nameT.setFixedSize(200, 20)
        grid.addWidget(self.nameT, 1, 2, 1, 2)

        self.importantB = QCheckBox(self, text="Important?")
        grid.addWidget(self.importantB, 1, 4)

        infoL = QLabel(self, text="Info:")
        grid.addWidget(infoL, 2, 1)

        self.infoT = QTextEdit(self)
        self.infoT.setFixedSize(300, 150)
        grid.addWidget(self.infoT, 2, 2, 1, 3)

        save = QPushButton(self, text="Save")
        save.clicked.connect(self.save)
        grid.addWidget(save, 4, 1)

        remove = QPushButton(self, text="Remove")
        remove.clicked.connect(self.remove)
        grid.addWidget(remove, 4, 2)

        back = QPushButton(self, text="Back")
        back.clicked.connect(self.back)
        grid.addWidget(back, 4, 3)

        names = json_reader.readCharNames()
        names.append("New")
        self.allChars = QComboBox(self)
        self.allChars.activated.connect((lambda: self.loadChar(self.allChars.currentText())))
        self.allChars.addItems(names)
        self.allChars.setCurrentIndex(self.allChars.count()-1)
        grid.addWidget(self.allChars, 4, 4)

    def loadChar(self, name):
        """
        Load one character from file based on the name.

        :param str name: name of character to load
        """
        print("Loading...")
        if self.importantB.isChecked():
            self.importantB.toggle()
        self.nameT.clear()
        self.infoT.clear()
        if name == "New":
            return
        characterL = json_reader.readOne(name, 'chars')
        charTL = Character(characterL["name"], characterL["desc"], characterL["important"])
        if charTL.getImportant():
            self.importantB.toggle()
        self.nameT.setText(charTL.getName())
        self.infoT.setText(charTL.getDesc())
        print("Loaded character " + self.allChars.currentText())

    def remove(self):
        """
        Remove a character from the list, and remove them from disk.
        """
        if not popup("Are you certain you want to completely remove this character?\n(Cannot be undone)",
                     "Warning"):
            return
        print("Removing character " + self.allChars.currentText())
        json_reader.deleteChar(self.allChars.currentText())
        self.allChars.removeItem(self.allChars.currentIndex())
        self.allChars.setCurrentIndex(self.allChars.count()-1)
        self.loadChar("New")

    def save(self):
        """
        Save a character to disk from the information entered in the GUI.
        """
        if self.nameT.text() in ["New", ""]:
            popup("Sorry, your character cannot be called \""+self.nameT.text()+"\". That is a reserved "
                  "keyword (and it's also a dumb name)", "Critical")
            return
        print("Saving")
        try:
            toWrite = Character(self.nameT.text(), self.infoT.toPlainText(), self.importantB.isChecked())
        except UnicodeEncodeError as e:
            print(e)
            print(type(e))
            popup("Sorry, unicode characters are not supported.", "Critical")
            return
        json_reader.writeOne(toWrite, 'chars')
        if toWrite.getName() not in [self.allChars.itemText(i) for i in range(self.allChars.count())]:
            self.allChars.insertItem(self.allChars.count()-1, self.nameT.text())
            self.allChars.setCurrentIndex(self.allChars.count()-2)
        print("Saved")

    def back(self):
        """
        Return the view to the calling widget.
        """
        self.mainframe.changeState(self.op)
Пример #12
0
class CreationContainer(QWidget):
    """
    Apparently just another level of parent class for social link display.
    Man I was stupid.
    Still am luigi2hands

    :param MainFrame mainframe: application mainframe
    :param QWidget op: parent widget
    :param int load: index to load maybe?
    """
    def __init__(self, mainframe, op, load):
        QWidget.__init__(self)
        self.mainframe = mainframe
        self.op = op
        self.op.cc = self
        self.load = load

        # View initializers...
        self.actions = None
        self.window = None

        self.initUI()
        self.op.grid.addWidget(self, 0, 0, 2, 10)

    def initUI(self):
        """
        Initialize the GUI.
        Does lots of stuff.
        """
        self.grid = QGridLayout()
        self.setLayout(self.grid)

        self.actions = self.op.link.getIDs()
        self.actions.append("New element")
        types = ["Info", "Speak", "Camera Change", "Movement"]

        self.save = QPushButton(self, text="Save")
        self.grid.addWidget(self.save, 3, 0)

        self.existing_connections = QListWidget(self)
        self.populateExistingConnections()
        self.grid.addWidget(self.existing_connections, 1, 5, 2, 1)

        self.next = QComboBox(self)
        self.next.addItems(self.actions)
        self.next.setMaximumWidth(150)
        if self.load != 0:
            self.next.setCurrentIndex(self.op.i)
        self.grid.addWidget(self.next, 3, 2)

        self.window = None

        self.actOM = QComboBox(self)
        self.actOM.addItems(types)
        self.actOM.activated.connect(self.changeFrame)
        self.grid.addWidget(self.actOM, 0, 0, 1, 2)

        self.connect()
        self.next.setCurrentIndex(self.next.count() - 1)

        self.backB = QPushButton(self, text="Back to List Menu")
        self.backB.clicked.connect(self.back)
        self.grid.addWidget(self.backB, 3, 4)

        self.lead = QLabel(self, text="Leads to:")
        self.lead.setAlignment(Qt.AlignRight)
        self.grid.addWidget(self.lead, 3, 1)

        self.connectB = QPushButton(self, text="Connect")
        self.connectB.clicked.connect(self.lightConnect)
        self.grid.addWidget(self.connectB, 3, 3)

        self.follow_path = QPushButton(self, text="Enter linked element")
        self.follow_path.clicked.connect(self.follow)
        self.grid.addWidget(self.follow_path, 0, 6, 2, 1)

        self.rmvRel = QPushButton(self, text="Remove this connection")
        self.rmvRel.clicked.connect(self.removeRelation)
        self.grid.addWidget(self.rmvRel, 1, 6, 2, 1)

        self.conLab = QLabel(self, text="This action connects to:")
        self.grid.addWidget(self.conLab, 0, 5)

    def removeRelation(self):
        """
        Remove a relation, which will also delete the uniquely dependant subtree.
        """
        if not self.existing_connections.currentItem() or \
           not popup("Are you sure you want to remove this relation? Any elements with a unique dependancy "
                     "on this relation will also be deleted.\nIt is highly recommended you take a look at "
                     "the graphical view of the tree in order to see the potential effects of the deletion.",
                     "Warning"):
            return
        self.op.link.delRelation(
            self.op.i,
            self.actions.index(self.existing_connections.currentItem().text()))
        self.populateExistingConnections()
        self.updateElementList()
        self.op.linkstored.save()

    def populateExistingConnections(self):
        """
        Display all the existing connections of the current node.
        """
        self.existing_connections.clear()
        for relation in self.op.link.getRelations(self.op.i):
            self.existing_connections.addItem(
                self.op.link.getOneID(self.op.link.getItem(relation)))

    def back(self):
        """
        Return to the higher-level cutscene container view...
        """
        if not popup("Return to list main menu?\n(Lose any unsaved changes)",
                     "Warning"):
            return
        self.close()
        self.op.cc = None
        self.op.viewF(False)

    def follow(self):
        """
        Move on to the edit view of the selected relationship.
        """
        if not self.existing_connections.currentItem() or \
           self.existing_connections.currentItem().text() == "":
            return
        self.next.setCurrentIndex([
            self.next.itemText(i) for i in range(self.next.count())
        ].index(self.existing_connections.currentItem().text()))
        self.op.i = self.actions.index(self.next.currentText())
        self.connect()
        self.next.setCurrentIndex(self.next.count() - 1)

    def lightConnect(self):
        """
        Create a relationship between the current action and another.
        """
        if not self.checkCached():
            popup("Please save this action before linking it to a new one",
                  "Information")
            return
        if self.next.currentText() == "New element":
            self.op.link.addRelation(self.op.i, self.op.link.size())
            print("Linked to index " + str(self.op.link.size()))
            self.op.i = self.op.link.size()
            self.load = 0
            self.changeFrame(0)
            self.updateElementList()
        else:
            self.op.link.addRelation(
                self.op.i, self.actions.index(self.next.currentText()))
            print("Linked to index " +
                  str(self.actions.index(self.next.currentText())))
        self.populateExistingConnections()

    def connect(self):
        """
        Create a relationship between the current action and another, and enter the edit view of the
        relationship.

        :raises Exception: if the requested new action's type can't be processed (should never happen)
        """
        print(self.next.currentText())
        if self.next.currentText() == "New element":
            self.load = 0
            self.changeFrame(0)
        else:
            if isinstance(
                    self.op.link.getItem(
                        self.actions.index(self.next.currentText())), Info):
                self.actOM.setCurrentIndex(0)
            elif isinstance(
                    self.op.link.getItem(
                        self.actions.index(self.next.currentText())), Speak):
                self.actOM.setCurrentIndex(1)
            elif isinstance(
                    self.op.link.getItem(
                        self.actions.index(self.next.currentText())), Camera):
                self.actOM.setCurrentIndex(2)
            elif isinstance(
                    self.op.link.getItem(
                        self.actions.index(self.next.currentText())),
                    Movement):
                self.actOM.setCurrentIndex(3)
            else:
                raise Exception("Not a type!")
            self.load = self.op.link.getItem(
                self.actions.index(self.next.currentText()))
            self.changeFrame(0)

    def checkCached(self):
        """
        Check if the current element has been saved before.

        :returns: if the element has been saved
        :rtype: bool
        """
        print(len(self.op.link.items) - 1)
        print(self.op.i)
        if self.op.link.getItem(self.op.i) == []:
            return False
        return True

    def updateElementList(self):
        """
        Update the relationships list, I think.
        """
        self.next.clear()
        self.actions = self.op.link.getIDs()
        self.actions.append("New element")
        self.next.addItems(self.actions)
        self.next.setCurrentIndex(len(self.actions) - 1)

    def changeFrame(self, _):
        """
        Change view to edit a certain type of action.

        :param objct _: unused, but required by caller
        """
        print("Changed to " + self.actOM.currentText())
        try:
            self.window.close()
        except AttributeError:
            pass  # No window open
        if self.actOM.currentText() == "Speak":
            self.window = SpeakFrame(self, self.load)
        elif self.actOM.currentText() == "Camera Change":
            self.window = CameraFrame(self, self.load)
        elif self.actOM.currentText() == "Movement":
            self.window = MoveFrame(self, self.load)
        else:  # self.actOM.currentText() == "Info":
            self.window = InfoFrame(self, self.load)
        try:
            self.save.clicked.disconnect()
        except:  #pylint: disable=bare-except
            pass
        self.save.clicked.connect(self.window.save)
        self.populateExistingConnections()
        self.updateElementList()
Пример #13
0
    def initUI(self):
        """
        Initializes the GUI.
        Does lots of stuff.
        """
        self.grid = QGridLayout()
        self.setLayout(self.grid)

        self.characs = json_reader.readCharNames()
        self.arcanas = json_reader.data_list("arcanas")
        self.arcanas.extend([""])

        self.textl = QLabel(self, text="Text:")
        self.grid.addWidget(self.textl, 1, 0)

        self.infoBox = QTextEdit(self)
        self.infoBox.setFixedSize(300, 150)
        self.grid.addWidget(self.infoBox, 1, 1, 3, 3)

        self.textl = QLabel(self, text="Points:")
        self.grid.addWidget(self.textl, 2, 4, 1, 2)

        self.pointBox = QLineEdit(self)
        self.pointBox.setFixedSize(20, 20)
        self.grid.addWidget(self.pointBox, 3, 4)
        self.pointvec.append(self.pointBox)

        pointOM = QComboBox(self)
        pointOM.addItems(self.arcanas)
        pointOM.setCurrentIndex(pointOM.count() - 1)
        self.grid.addWidget(pointOM, 3, 5)
        self.pointvar.append(pointOM)

        self.textl = QLabel(self, text="Angle:")
        self.grid.addWidget(self.textl, 2, 6, 1, 2)

        angleBox = QLineEdit(self)
        angleBox.setFixedSize(20, 20)
        self.grid.addWidget(angleBox, 3, 6)
        self.anglevec.append(angleBox)

        angleOM = QComboBox(self)
        angleOM.addItems(self.arcanas)
        angleOM.setCurrentIndex(angleOM.count() - 1)
        self.grid.addWidget(angleOM, 3, 7)
        self.anglevar.append(angleOM)

        self.speakerl = QLabel(self, text="Speaker:")
        self.grid.addWidget(self.speakerl, 1, 4)

        self.speaker = QComboBox(self)
        self.speaker.addItems(self.characs)
        self.grid.addWidget(self.speaker, 1, 5)

        self.emotionL = QLabel(self, text="Emotion:")  #emotionL memes
        self.grid.addWidget(self.emotionL, 1, 6)
        self.s_emotions = json_reader.data_list('sprite_emotions')
        self.emotion = QComboBox(self)
        self.emotion.addItems(self.s_emotions)
        self.grid.addWidget(self.emotion, 1, 7)

        self.addp = QPushButton(self, text="Add points")
        self.addp.clicked.connect(self.extendP)
        self.grid.addWidget(self.addp, 30, 4, 1, 2)

        self.adda = QPushButton(self, text="Add angles")
        self.adda.clicked.connect(self.extendA)
        self.grid.addWidget(self.adda, 30, 6, 1, 2)

        if self.load != 0:
            self.infoBox.setText(self.load.getText())
            self.speaker.setCurrentIndex(
                self.characs.index(self.load.getSpeaker()))
            self.emotion.setCurrentIndex(
                self.s_emotions.index(self.load.emotion))
            first = True
            for arcana, points in self.load.getPoints().items():
                if first:
                    first = False
                else:
                    self.extendP()
                self.pointvec[-1].setText(str(points))
                self.pointvar[-1].setCurrentIndex(self.arcanas.index(arcana))
            first = True
            for arcana, angle in self.load.getAngle().items():
                if first:
                    first = False
                else:
                    self.extendA()
                self.anglevec[-1].setText(str(angle))
                self.anglevar[-1].setCurrentIndex(self.arcanas.index(arcana))
Пример #14
0
class PersonaUI(QWidget):
    """
    Widget for Persona creation view.

    :param MainFrame mainframe: application mainframe
    :param QWidget op: parent widget
    """
    def __init__(self, mainframe, op):
        QWidget.__init__(self)
        self.mainframe = mainframe
        self.op = op

        self.grid = QGridLayout()
        self.setLayout(self.grid)

        self.listP = None
        self.listLS = None
        self.listEL1 = None
        self.listEL2 = None

        self.nameT = None
        self.levelT = None
        self.textT = None
        self.strT = None
        self.magT = None
        self.endT = None
        self.agiT = None
        self.luckT = None

        self.createFrame = None
        self.buttonFrame = None
        self.bfgrid = None
        # Actual create frame variables.
        self.cfgrid = None
        self.lsdic = None
        self.slashO = None
        self.strikeO = None
        self.pierceO = None
        self.fireO = None
        self.iceO = None
        self.windO = None
        self.elecO = None
        self.darkO = None
        self.lightO = None
        self.arcO = None
        self.iSpellOs = None
        self.lsSpellO = None
        self.lslevel = None


        self.initUI(True)

    def initUI(self, infoDump):
        """
        Initializes the basic UI showing the list of Personas.
        Does a lot of stuff.

        :param dict infoDump: not sure lol
        """
        self.mainframe.setWindowTitle("Persona Creator")

        if not infoDump:
            self.createFrameDraw()

        self.initButtonFrame(infoDump)

        self.listP = QListWidget(self)
        self.grid.addWidget(self.listP, 0, 3, 2, 1)
        temp = json_reader.readPerNames()
        self.listP.addItems(temp)

    def initButtonFrame(self, infoDump):
        """
        Initializes the buttonframes that are present in all Persona creator views.

        :param dict infoDump: not sure lol
        """
        self.buttonFrame = QWidget(self)
        self.bfgrid = QGridLayout()
        self.buttonFrame.setLayout(self.bfgrid)

        self.grid.addWidget(self.buttonFrame, 3, 0, 1, 4)


        new = QPushButton(self.buttonFrame, text="New")
        new.clicked.connect(self.new)
        self.bfgrid.addWidget(new, 4, 0)

        back = QPushButton(self.buttonFrame, text="Back")
        back.clicked.connect(self.back)
        self.bfgrid.addWidget(back, 4, 4)

        remove = QPushButton(self.buttonFrame, text="Remove")
        remove.clicked.connect(self.remove)
        self.bfgrid.addWidget(remove, 4, 3)

        edit = QPushButton(self.buttonFrame, text="Edit")
        edit.clicked.connect(self.edit)
        self.bfgrid.addWidget(edit, 4, 2)

        if not infoDump:
            save = QPushButton(self.buttonFrame, text="Save")
            save.clicked.connect(self.save)
            self.bfgrid.addWidget(save, 4, 1)


    def createFrameDraw(self):
        """
        Initializes the GUI of the actual creation frame view.
        Does a LOT of stuff.
        """
        self.createFrame = QWidget(self)
        self.cfgrid = QGridLayout()
        self.createFrame.setLayout(self.cfgrid)
        self.grid.addWidget(self.createFrame, 0, 0, 2, 2)

        self.lsdic = {}

        nameL = QLabel(self.createFrame, text="Name:")
        self.cfgrid.addWidget(nameL, 0, 0)
        self.nameT = QLineEdit(self.createFrame)
        self.nameT.setFixedSize(100, 20)
        self.cfgrid.addWidget(self.nameT, 0, 1)

        strL = QLabel(self.createFrame, text="Str")
        self.cfgrid.addWidget(strL, 0, 2)
        self.strT = QLineEdit(self.createFrame)
        self.strT.setFixedSize(20, 20)
        self.cfgrid.addWidget(self.strT, 0, 3)
        magL = QLabel(self.createFrame, text="Mag")
        self.cfgrid.addWidget(magL, 1, 2)
        self.magT = QLineEdit(self.createFrame)
        self.magT.setFixedSize(20, 20)
        self.cfgrid.addWidget(self.magT, 1, 3)
        endL = QLabel(self.createFrame, text="End")
        self.cfgrid.addWidget(endL, 2, 2)
        self.endT = QLineEdit(self.createFrame)
        self.endT.setFixedSize(20, 20)
        self.cfgrid.addWidget(self.endT, 2, 3)
        agiL = QLabel(self.createFrame, text="Agi")
        self.cfgrid.addWidget(agiL, 3, 2)
        self.agiT = QLineEdit(self.createFrame)
        self.agiT.setFixedSize(20, 20)
        self.cfgrid.addWidget(self.agiT, 3, 3)
        luckL = QLabel(self.createFrame, text="Luck")
        self.cfgrid.addWidget(luckL, 4, 2)
        self.luckT = QLineEdit(self.createFrame)
        self.luckT.setFixedSize(20, 20)
        self.cfgrid.addWidget(self.luckT, 4, 3)

        resList = json_reader.data_list("resistances")
        resL = QLabel(self.createFrame, text="Resistance:")
        self.cfgrid.addWidget(resL, 0, 5)
        slashL = QLabel(self.createFrame, text="Slash")
        self.cfgrid.addWidget(slashL, 1, 5)
        self.slashO = QComboBox(self.createFrame)
        self.slashO.addItems(resList)
        self.slashO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.slashO, 1, 6)
        strikeL = QLabel(self.createFrame, text="Strike")
        self.cfgrid.addWidget(strikeL, 2, 5)
        self.strikeO = QComboBox(self.createFrame)
        self.strikeO.addItems(resList)
        self.strikeO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.strikeO, 2, 6)
        pierceL = QLabel(self.createFrame, text="Pierce")
        self.cfgrid.addWidget(pierceL, 3, 5)
        self.pierceO = QComboBox(self.createFrame)
        self.pierceO.addItems(resList)
        self.pierceO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.pierceO, 3, 6)
        fireL = QLabel(self.createFrame, text="Fire")
        self.cfgrid.addWidget(fireL, 4, 5)
        self.fireO = QComboBox(self.createFrame)
        self.fireO.addItems(resList)
        self.fireO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.fireO, 4, 6)
        iceL = QLabel(self.createFrame, text="Ice")
        self.cfgrid.addWidget(iceL, 5, 5)
        self.iceO = QComboBox(self.createFrame)
        self.iceO.addItems(resList)
        self.iceO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.iceO, 5, 6)
        elecL = QLabel(self.createFrame, text="Elec")
        self.cfgrid.addWidget(elecL, 6, 5)
        self.elecO = QComboBox(self.createFrame)
        self.elecO.addItems(resList)
        self.elecO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.elecO, 6, 6)
        windL = QLabel(self.createFrame, text="Wind")
        self.cfgrid.addWidget(windL, 7, 5)
        self.windO = QComboBox(self.createFrame)
        self.windO.addItems(resList)
        self.windO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.windO, 7, 6)
        lightL = QLabel(self.createFrame, text="Light")
        self.cfgrid.addWidget(lightL, 8, 5)
        self.lightO = QComboBox(self.createFrame)
        self.lightO.addItems(resList)
        self.lightO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.lightO, 8, 6)
        darkL = QLabel(self.createFrame, text="Dark")
        self.cfgrid.addWidget(darkL, 9, 5)
        self.darkO = QComboBox(self.createFrame)
        self.darkO.addItems(resList)
        self.darkO.setCurrentIndex(1)
        self.cfgrid.addWidget(self.darkO, 9, 6)

        spellList = json_reader.data_list("spells")
        self.listLS = QListWidget(self.createFrame)
        self.listLS.setFixedSize(200, 300)
        self.cfgrid.addWidget(self.listLS, 3, 7, 8, 2)

        newLS = QPushButton(self.createFrame, text="+")
        newLS.clicked.connect(self.addLS)
        self.cfgrid.addWidget(newLS, 2, 7)
        delLS = QPushButton(self.createFrame, text="DEL")
        delLS.clicked.connect(self.delLS)
        self.cfgrid.addWidget(delLS, 2, 8)

        lsl = QLabel(self.createFrame, text="Learned Spells:")
        self.cfgrid.addWidget(lsl, 0, 7, 1, 2)

        arcanaL = QLabel(self.createFrame, text="Arcana:")
        self.cfgrid.addWidget(arcanaL, 1, 0)
        arc_list = json_reader.data_list("arcanas")
        self.arcO = QComboBox(self.createFrame)
        self.arcO.addItems(arc_list)
        self.arcO.setCurrentIndex(0)
        self.cfgrid.addWidget(self.arcO, 1, 1)

        levelL = QLabel(self.createFrame, text="Level:")
        self.cfgrid.addWidget(levelL, 2, 0)
        self.levelT = QLineEdit(self.createFrame)
        self.levelT.setFixedSize(20, 20)
        self.cfgrid.addWidget(self.levelT, 2, 1)

        heritageL = QLabel(self.createFrame, text="Inherits:")
        self.cfgrid.addWidget(heritageL, 3, 0, 1, 2)

        elements = json_reader.data_list("elements")
        elements.append("Support")
        self.listEL1 = QComboBox(self.createFrame)
        self.listEL1.addItems(elements)
        self.cfgrid.addWidget(self.listEL1, 4, 0)
        self.listEL2 = QComboBox(self.createFrame)
        self.listEL2.addItems(elements)
        self.cfgrid.addWidget(self.listEL2, 4, 1)

        iSpellL = QLabel(self.createFrame, text="Initial Spells:")
        self.cfgrid.addWidget(iSpellL, 5, 0, 1, 2)
        self.iSpellOs = []
        for i in range(6, 9):
            temp = QComboBox(self.createFrame)
            temp.addItems(spellList)
            temp2 = QComboBox(self.createFrame)
            temp2.addItems(spellList)
            self.cfgrid.addWidget(temp, i, 0, 1, 2)
            self.cfgrid.addWidget(temp2, i, 2, 1, 2)
            self.iSpellOs.extend([temp, temp2])

        textL = QLabel(self.createFrame, text="Info:")
        self.cfgrid.addWidget(textL, 10, 0)
        self.textT = QTextEdit(self.createFrame)
        self.textT.setFixedSize(300, 100)
        self.cfgrid.addWidget(self.textT, 10, 1, 1, 5)

        self.lslevel = QLineEdit(self.createFrame)
        self.lslevel.setFixedSize(40, 20)
        self.lsSpellO = QComboBox(self.createFrame)
        self.lsSpellO.addItems(spellList)

        self.cfgrid.addWidget(self.lsSpellO, 1, 7)
        self.cfgrid.addWidget(self.lslevel, 1, 8)

    def addLS(self):
        """
        Add a learned spell to the list, based on what was entered.
        """
        print("Adding learned spell")
        chosenSpell = self.lsSpellO.currentText()
        if (int)(self.lslevel.text()) <= (int)(self.levelT.text()):
            popup("You cannot add a spell at an earlier level than the Persona's base level", "Critical")
            return
        if chosenSpell != "":
            print("Ok")
            self.lsdic[chosenSpell] = self.lslevel.text()
            self.listLS.addItem(chosenSpell + " at level " + self.lslevel.text())
            self.lslevel.setText("")
            self.lsSpellO.setCurrentIndex(0)
            return
        popup("You must choose a spell", "Critical")

    def delLS(self):
        """
        Remove the selected learned spell from the list
        """
        print("Deleting learned spell")
        key = ""
        i = 0
        while len(self.listLS.currentItem().text()) > i:
            if self.listLS.currentItem().text()[i] == " " and \
               self.listLS.currentItem().text()[i+1] == "a" and \
               self.listLS.currentItem().text()[i+2] == "t":  # TODO EWWWWWW
                break
            key += self.listLS.currentItem().text()[i]
            i = i + 1
        print(key)
        print(self.lsdic.pop(key))
        self.listLS.takeItem(self.listLS.currentRow())


    def loadPer(self, name):
        """
        Load a certain Persona from file.

        :param str name: name of Persona to load
        """
        data = json_reader.readOne(name, 'pers')
        self.nameT.setText(data["name"])
        self.textT.setText(data["desc"])
        self.strT.setText(data["stats"][0])
        self.magT.setText(data["stats"][1])
        self.endT.setText(data["stats"][2])
        self.agiT.setText(data["stats"][3])
        self.luckT.setText(data["stats"][4])
        self.levelT.setText(data["level"])

        self.arcO.setCurrentIndex(
            [self.arcO.itemText(i) for i in range(self.arcO.count())].index(data["arcana"])
        )

        self.listEL1.setCurrentIndex(
            [self.listEL1.itemText(i) for i in range(self.listEL1.count())].index(data["heritage"][0])
        )
        self.listEL2.setCurrentIndex(
            [self.listEL2.itemText(i) for i in range(self.listEL2.count())].index(data["heritage"][1])
        )

        self.slashO.setCurrentIndex(
            [self.slashO.itemText(i) for i in range(self.slashO.count())].index(data["resistance"][0])
        )
        self.strikeO.setCurrentIndex(
            [self.strikeO.itemText(i) for i in range(self.strikeO.count())].index(data["resistance"][1])
        )
        self.pierceO.setCurrentIndex(
            [self.pierceO.itemText(i) for i in range(self.pierceO.count())].index(data["resistance"][2])
        )
        self.fireO.setCurrentIndex(
            [self.fireO.itemText(i) for i in range(self.fireO.count())].index(data["resistance"][3])
        )
        self.iceO.setCurrentIndex(
            [self.iceO.itemText(i) for i in range(self.iceO.count())].index(data["resistance"][4])
        )
        self.elecO.setCurrentIndex(
            [self.elecO.itemText(i) for i in range(self.elecO.count())].index(data["resistance"][5])
        )
        self.windO.setCurrentIndex(
            [self.windO.itemText(i) for i in range(self.windO.count())].index(data["resistance"][6])
        )
        self.lightO.setCurrentIndex(
            [self.lightO.itemText(i) for i in range(self.lightO.count())].index(data["resistance"][7])
        )
        self.darkO.setCurrentIndex(
            [self.darkO.itemText(i) for i in range(self.darkO.count())].index(data["resistance"][8])
        )

        i = 0
        for combobox in self.iSpellOs:
            combobox.setCurrentIndex(
                [combobox.itemText(j) for j in range(combobox.count()-1)].index(data["spellDeck"][i])
            )
            i += 1

        self.lsdic = data["spellLearn"]
        self.listLS.clear()
        for spell, level in self.lsdic.items():
            self.listLS.addItem(spell + " at level " + level)

        print("Loaded " + data["name"])

    def edit(self):
        """
        Switch to edit view, also loads the selected Persona.
        """
        try:
            if self.listP.currentItem().text() != "":
                if self.createFrame and not popup("Override any unsaved changes?", "Warning"):
                    return
                self.loadPer(self.listP.currentItem().text())
        except AttributeError:  # To initialize createFrame UI before load
            if self.listP.currentItem().text() != "":
                temp = self.listP.currentItem().text()
                self.buttonFrame.close()
                self.initUI(False)
                self.loadPer(temp)
            else:
                return
        self.createFrame.show()
        self.mainframe.center()
        print("Changed to edit frame")

    def save(self):
        """
        Validate all info and save to file on disk.
        """
        if os.path.exists(json_reader.buildPath("data/pers/"+self.nameT.text()+".json")):
            if not popup("Override existing Persona "+self.nameT.text()+"?", "Question"):
                return
        print("Saving")
        spellDeck = []
        for combobox in self.iSpellOs:
            spellDeck.append(combobox.currentText())
        stats = [self.strT.text(), self.magT.text(), self.endT.text(), self.agiT.text(), self.luckT.text()]
        res = [self.slashO.currentText(), self.strikeO.currentText(), self.pierceO.currentText(),
               self.fireO.currentText(), self.iceO.currentText(), self.elecO.currentText(),
               self.windO.currentText(), self.lightO.currentText(), self.darkO.currentText()]
        try:
            (int)(self.levelT.text())
            (int)(self.strT.text())
            (int)(self.magT.text())
            (int)(self.endT.text())
            (int)(self.agiT.text())
            (int)(self.luckT.text())
        except ValueError:
            popup("There is a number entry that isn't valid.\nEntries requiring numbers are:\nLEVEL\nSTR"
                  "\nMAG\nEND\nAGI\nLUCK", "Critical")
            print("Not Saved")
            return
        if not (self.nameT.text() and not self.nameT.text().isspace()):
            popup("No name entered for your Persona. Name is a required field.", "Critical")
            print("No Name, not saved")
            return
        toWrite = Persona(
            self.nameT.text(),
            self.arcO.currentText(),
            self.levelT.text(),
            self.textT.toPlainText(),
            spellDeck,
            self.lsdic,
            stats,
            res,
            [self.listEL1.currentText(), self.listEL2.currentText()]
        )
        json_reader.writeOne(toWrite, 'pers')
        temp = self.nameT.text()
        if (temp not in [self.listP.item(i).text() for i in range(self.listP.count())]):
            self.listP.addItem(temp)
        self.loadPer(temp)
        print("Saved Persona")

    def remove(self):
        """
        Remove a created Persona from the list and delete the file on disk.
        """
        if self.listP.currentItem().text() == "":
            return
        if not popup(
                "Are you certain you want to completely remove this Persona?\n(Cannot be undone)", "Warning"
            ):
            return
        print("Removing Persona " + self.listP.currentItem().text())
        json_reader.deletePer(self.listP.currentItem().text())
        self.listP.takeItem(
            [self.listP.item(i).text() for i in range(self.listP.count())].index(
                self.listP.currentItem().text())
        )

    def new(self):
        """
        Open an empty Persona edit view.
        """
        if self.createFrame and not popup("Override any unsaved changes?", "Warning"):
            return
        if self.createFrame:
            self.createFrame.close()
        self.buttonFrame.close()
        self.initUI(False)
        self.createFrame.show()
        self.mainframe.center()
        print("Created")

    def back(self):
        """
        Return to the parent widget.
        """
        print("Returned to main screen")
        self.mainframe.changeState(self.op)
Пример #15
0
class QGroundObjectBuyMenu(QDialog):
    layout_changed_signal = Signal(QTgoLayout)

    def __init__(
        self,
        parent: QWidget,
        ground_object: TheaterGroundObject,
        game: Game,
        current_group_value: int,
    ) -> None:
        super().__init__(parent)

        self.setMinimumWidth(350)

        self.setWindowTitle("Buy ground object @ " + ground_object.obj_name)
        self.setWindowIcon(EVENT_ICONS["capture"])

        self.mainLayout = QGridLayout()
        self.setLayout(self.mainLayout)

        self.force_group_selector = QComboBox()
        self.force_group_selector.setMinimumWidth(250)
        self.layout_selector = QComboBox()
        self.layout_selector.setMinimumWidth(250)

        # Get the layouts and fill the combobox
        tasks = []
        if isinstance(ground_object, SamGroundObject):
            role = GroupRole.AIR_DEFENSE
        elif isinstance(ground_object, VehicleGroupGroundObject):
            role = GroupRole.GROUND_FORCE
        elif isinstance(ground_object, EwrGroundObject):
            role = GroupRole.AIR_DEFENSE
            tasks.append(GroupTask.EARLY_WARNING_RADAR)
        else:
            raise NotImplementedError(
                f"Unhandled TGO type {ground_object.__class__}")

        if not tasks:
            tasks = role.tasks

        for group in game.blue.armed_forces.groups_for_tasks(tasks):
            self.force_group_selector.addItem(group.name, userData=group)
        self.force_group_selector.setEnabled(
            self.force_group_selector.count() > 1)
        self.force_group_selector.adjustSize()
        force_group = self.force_group_selector.itemData(
            self.force_group_selector.currentIndex())

        for layout in force_group.layouts:
            self.layout_selector.addItem(layout.name, userData=layout)
        self.layout_selector.adjustSize()
        self.layout_selector.setEnabled(len(force_group.layouts) > 1)
        selected_template = self.layout_selector.itemData(
            self.layout_selector.currentIndex())

        self.theater_layout = QTgoLayout(selected_template, force_group)

        self.layout_selector.currentIndexChanged.connect(self.layout_changed)
        self.force_group_selector.currentIndexChanged.connect(
            self.force_group_changed)

        template_selector_layout = QGridLayout()
        template_selector_layout.addWidget(QLabel("Armed Forces Group:"), 0, 0,
                                           Qt.AlignLeft)
        template_selector_layout.addWidget(self.force_group_selector,
                                           0,
                                           1,
                                           alignment=Qt.AlignRight)
        template_selector_layout.addWidget(QLabel("Layout:"), 1, 0,
                                           Qt.AlignLeft)
        template_selector_layout.addWidget(self.layout_selector,
                                           1,
                                           1,
                                           alignment=Qt.AlignRight)
        self.mainLayout.addLayout(template_selector_layout, 0, 0)

        self.template_layout = QGroundObjectTemplateLayout(
            game,
            ground_object,
            self.theater_layout,
            self.layout_changed_signal,
            current_group_value,
        )
        self.template_layout.close_dialog_signal.connect(self.close_dialog)
        self.mainLayout.addWidget(self.template_layout, 1, 0)
        self.setLayout(self.mainLayout)

    def force_group_changed(self) -> None:
        # Prevent ComboBox from firing change Events
        self.layout_selector.blockSignals(True)
        unit_group = self.force_group_selector.itemData(
            self.force_group_selector.currentIndex())
        self.layout_selector.clear()
        for layout in unit_group.layouts:
            self.layout_selector.addItem(layout.name, userData=layout)
        self.layout_selector.adjustSize()
        # Enable if more than one template is available
        self.layout_selector.setEnabled(len(unit_group.layouts) > 1)
        # Enable Combobox Signals again
        self.layout_selector.blockSignals(False)
        self.layout_changed()

    def layout_changed(self) -> None:
        self.layout()
        self.theater_layout.layout = self.layout_selector.itemData(
            self.layout_selector.currentIndex())
        self.theater_layout.force_group = self.force_group_selector.itemData(
            self.force_group_selector.currentIndex())
        self.layout_changed_signal.emit(self.theater_layout)

    def close_dialog(self) -> None:
        self.accept()
Пример #16
0
class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()

        self.setWindowTitle('Voice Player')
        self.words = []

        centralWidget = QWidget()
        self.setCentralWidget(centralWidget)
        layout = QFormLayout(centralWidget)

        self.readButton = QPushButton('Read CSV')
        self.readButton.clicked.connect(self.readCSV)
        layout.addRow('CSV File:', self.readButton)

        textLayout = QHBoxLayout()
        self.text = QLineEdit('')
        self.text.setClearButtonEnabled(True)
        textLayout.addWidget(self.text)
        self.sayButton = QPushButton('Say')
        textLayout.addWidget(self.sayButton)
        self.text.returnPressed.connect(self.sayButton.animateClick)
        self.sayButton.clicked.connect(self.say)
        layout.addRow('Text:', textLayout)

        self.voiceCombo = QComboBox()
        self.voiceCombo.currentIndexChanged.connect(self.indexChange)
        layout.addRow('Voice:', self.voiceCombo)

        self.volumeSlider = QSlider(Qt.Horizontal)
        self.volumeSlider.setMinimum(0)
        self.volumeSlider.setMaximum(100)
        self.volumeSlider.setValue(100)
        layout.addRow('Volume:', self.volumeSlider)

        self.engine = None
        engineNames = QTextToSpeech.availableEngines()
        if len(engineNames) > 0:
            engineName = engineNames[0]
            self.engine = QTextToSpeech(engineName)
            self.engine.stateChanged.connect(self.stateChanged)
            #self.setWindowTitle('QTextToSpeech Example ({})'.format(engineName))
            #self.voices = []
            #for voice in self.engine.availableVoices():
            #    self.voices.append(voice)
            #    self.voiceCombo.addItem(voice.name())
            self.voice = self.engine.availableVoices()[0]
        else:
            self.setWindowTitle('QTextToSpeech Example (no engines available)')
            self.sayButton.setEnabled(False)

    def indexChange(self, index):
        word = self.words[self.voiceCombo.currentIndex()]
        self.text.setText(word)

    def readCSV(self):
        fileDialog = QFileDialog(self)
        fileDialog.setNameFilters(['CSV File (*.csv)'])
        if fileDialog.exec_() == QDialog.Accepted:
            csvPath = fileDialog.selectedFiles()[0]
            self.words = []
            with open(csvPath) as fi:
                for line in fi:
                    line = line.strip()
                    if not line: continue
                    self.words.append(line)
            self.voiceCombo.clear()
            for word in self.words:
                self.voiceCombo.addItem(word)
            if len(self.words):
                word = self.words[self.voiceCombo.currentIndex()]
                self.text.setText(word)

    def say(self):
        self.sayButton.setEnabled(False)
        self.engine.setVoice(self.voice)
        self.engine.setVolume(float(self.volumeSlider.value()) / 100)
        text = self.text.text().strip()
        if not text:
            self.sayButton.setEnabled(True)
        else:
            self.engine.say(self.text.text())

    def stateChanged(self, state):
        if (state == QTextToSpeech.State.Ready):
            self.sayButton.setEnabled(True)
            currIdx = self.voiceCombo.currentIndex()
            count = self.voiceCombo.count()
            if currIdx + 1 < count:
                self.voiceCombo.setCurrentIndex(currIdx + 1)
                word = self.words[self.voiceCombo.currentIndex()]
                self.text.setText(word)
            else:
                self.text.setText('')
Пример #17
0
class GrainSizeDatasetViewer(QDialog):
    PAGE_ROWS = 20
    logger = logging.getLogger("root.ui.GrainSizeDatasetView")
    gui_logger = logging.getLogger("GUI")

    def __init__(self, parent=None):
        super().__init__(parent=parent, f=Qt.Window)
        self.setWindowTitle(self.tr("Grain-size Dataset Viewer"))
        self.__dataset = GrainSizeDataset()  # type: GrainSizeDataset
        self.init_ui()
        self.data_table.setRowCount(0)
        self.frequency_curve_chart = FrequencyCurveChart(parent=self,
                                                         toolbar=True)
        self.frequency_curve_3D_chart = FrequencyCurve3DChart(parent=self,
                                                              toolbar=True)
        self.cumulative_curve_chart = CumulativeCurveChart(parent=self,
                                                           toolbar=True)
        self.folk54_GSM_diagram_chart = Folk54GSMDiagramChart(parent=self,
                                                              toolbar=True)
        self.folk54_SSC_diagram_chart = Folk54SSCDiagramChart(parent=self,
                                                              toolbar=True)
        self.BP12_GSM_diagram_chart = BP12GSMDiagramChart(parent=self,
                                                          toolbar=True)
        self.BP12_SSC_diagram_chart = BP12SSCDiagramChart(parent=self,
                                                          toolbar=True)
        self.load_dataset_dialog = LoadDatasetDialog(parent=self)
        self.load_dataset_dialog.dataset_loaded.connect(self.on_data_loaded)
        self.file_dialog = QFileDialog(parent=self)
        self.normal_msg = QMessageBox(self)

    def init_ui(self):
        self.setWindowTitle(self.tr("Dataset Viewer"))
        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.data_table.hideColumn(0)
        self.main_layout = QGridLayout(self)
        self.main_layout.addWidget(self.data_table, 0, 0, 1, 3)

        self.previous_button = QPushButton(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.currentIndexChanged.connect(
            self.update_page)
        self.next_button = QPushButton(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.geometric_checkbox = QCheckBox(self.tr("Geometric"))
        self.geometric_checkbox.setChecked(True)
        self.geometric_checkbox.stateChanged.connect(
            self.on_is_geometric_changed)
        self.main_layout.addWidget(self.geometric_checkbox, 2, 0)
        self.FW57_checkbox = QCheckBox(self.tr("Method of statistic moments"))
        self.FW57_checkbox.setChecked(False)
        self.FW57_checkbox.stateChanged.connect(self.on_is_FW57_changed)
        self.main_layout.addWidget(self.FW57_checkbox, 2, 1)
        self.proportion_combo_box = QComboBox()
        self.supported_proportions = [
            ("GSM_proportion", self.tr("Gravel, Sand, Mud")),
            ("SSC_proportion", self.tr("Sand, Silt, Clay")),
            ("BGSSC_proportion", self.tr("Boulder, Gravel, Sand, Silt, Clay"))
        ]
        self.proportion_combo_box.addItems(
            [description for _, description in self.supported_proportions])
        self.proportion_combo_box.currentIndexChanged.connect(
            lambda: self.update_page(self.page_index))
        self.main_layout.addWidget(self.proportion_combo_box, 2, 2)

        self.menu = QMenu(self.data_table)
        self.load_dataset_action = self.menu.addAction(qta.icon("fa.database"),
                                                       self.tr("Load Dataset"))
        self.load_dataset_action.triggered.connect(self.load_dataset)
        self.plot_cumulative_curve_menu = self.menu.addMenu(
            qta.icon("mdi.chart-bell-curve-cumulative"),
            self.tr("Plot Cumlulative Curve Chart"))
        self.cumulative_plot_selected_action = self.plot_cumulative_curve_menu.addAction(
            self.tr("Plot Selected Samples"))
        self.cumulative_plot_selected_action.triggered.connect(
            lambda: self.plot_chart(self.cumulative_curve_chart, self.
                                    selections, False))
        self.cumulative_append_selected_action = self.plot_cumulative_curve_menu.addAction(
            self.tr("Append Selected Samples"))
        self.cumulative_append_selected_action.triggered.connect(
            lambda: self.plot_chart(self.cumulative_curve_chart, self.
                                    selections, True))
        self.cumulative_plot_all_action = self.plot_cumulative_curve_menu.addAction(
            self.tr("Plot All Samples"))
        self.cumulative_plot_all_action.triggered.connect(
            lambda: self.plot_chart(self.cumulative_curve_chart, self.__dataset
                                    .samples, False))
        self.cumulative_append_all_action = self.plot_cumulative_curve_menu.addAction(
            self.tr("Append All Samples"))
        self.cumulative_append_all_action.triggered.connect(
            lambda: self.plot_chart(self.cumulative_curve_chart, self.__dataset
                                    .samples, True))

        self.plot_frequency_curve_menu = self.menu.addMenu(
            qta.icon("mdi.chart-bell-curve"),
            self.tr("Plot Frequency Curve Chart"))
        self.frequency_plot_selected_action = self.plot_frequency_curve_menu.addAction(
            self.tr("Plot Selected Samples"))
        self.frequency_plot_selected_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_chart, self.
                                    selections, False))
        self.frequency_append_selected_action = self.plot_frequency_curve_menu.addAction(
            self.tr("Append Selected Samples"))
        self.frequency_append_selected_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_chart, self.
                                    selections, True))
        self.frequency_plot_all_action = self.plot_frequency_curve_menu.addAction(
            self.tr("Plot All Samples"))
        self.frequency_plot_all_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_chart, self.__dataset.
                                    samples, False))
        self.frequency_append_all_action = self.plot_frequency_curve_menu.addAction(
            self.tr("Append All Samples"))
        self.frequency_append_all_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_chart, self.__dataset.
                                    samples, True))

        self.plot_frequency_curve_3D_menu = self.menu.addMenu(
            qta.icon("mdi.video-3d"), self.tr("Plot Frequency Curve 3D Chart"))
        self.frequency_3D_plot_selected_action = self.plot_frequency_curve_3D_menu.addAction(
            self.tr("Plot Selected Samples"))
        self.frequency_3D_plot_selected_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_3D_chart, self.
                                    selections, False))
        self.frequency_3D_append_selected_action = self.plot_frequency_curve_3D_menu.addAction(
            self.tr("Append Selected Samples"))
        self.frequency_3D_append_selected_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_3D_chart, self.
                                    selections, True))
        self.frequency_3D_plot_all_action = self.plot_frequency_curve_3D_menu.addAction(
            self.tr("Plot All Samples"))
        self.frequency_3D_plot_all_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_3D_chart, self.
                                    __dataset.samples, False))
        self.frequency_3D_append_all_action = self.plot_frequency_curve_3D_menu.addAction(
            self.tr("Append All Samples"))
        self.frequency_3D_append_all_action.triggered.connect(
            lambda: self.plot_chart(self.frequency_curve_3D_chart, self.
                                    __dataset.samples, True))

        self.folk54_GSM_diagram_menu = self.menu.addMenu(
            qta.icon("mdi.triangle-outline"),
            self.tr("Plot GSM Diagram (Folk, 1954)"))
        self.folk54_GSM_plot_selected_action = self.folk54_GSM_diagram_menu.addAction(
            self.tr("Plot Selected Samples"))
        self.folk54_GSM_plot_selected_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_GSM_diagram_chart, self.
                                    selections, False))
        self.folk54_GSM_append_selected_action = self.folk54_GSM_diagram_menu.addAction(
            self.tr("Append Selected Samples"))
        self.folk54_GSM_append_selected_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_GSM_diagram_chart, self.
                                    selections, True))
        self.folk54_GSM_plot_all_action = self.folk54_GSM_diagram_menu.addAction(
            self.tr("Plot All Samples"))
        self.folk54_GSM_plot_all_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_GSM_diagram_chart, self.
                                    __dataset.samples, False))
        self.folk54_GSM_append_all_action = self.folk54_GSM_diagram_menu.addAction(
            self.tr("Append All Samples"))
        self.folk54_GSM_append_all_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_GSM_diagram_chart, self.
                                    __dataset.samples, True))

        self.folk54_SSC_diagram_menu = self.menu.addMenu(
            qta.icon("mdi.triangle-outline"),
            self.tr("Plot SSC Diagram (Folk, 1954)"))
        self.folk54_SSC_plot_selected_action = self.folk54_SSC_diagram_menu.addAction(
            self.tr("Plot Selected Samples"))
        self.folk54_SSC_plot_selected_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_SSC_diagram_chart, self.
                                    selections, False))
        self.folk54_SSC_append_selected_action = self.folk54_SSC_diagram_menu.addAction(
            self.tr("Append Selected Samples"))
        self.folk54_SSC_append_selected_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_SSC_diagram_chart, self.
                                    selections, True))
        self.folk54_SSC_plot_all_action = self.folk54_SSC_diagram_menu.addAction(
            self.tr("Plot All Samples"))
        self.folk54_SSC_plot_all_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_SSC_diagram_chart, self.
                                    __dataset.samples, False))
        self.folk54_SSC_append_all_action = self.folk54_SSC_diagram_menu.addAction(
            self.tr("Append All Samples"))
        self.folk54_SSC_append_all_action.triggered.connect(
            lambda: self.plot_chart(self.folk54_SSC_diagram_chart, self.
                                    __dataset.samples, True))

        self.BP12_GSM_diagram_menu = self.menu.addMenu(
            qta.icon("mdi.triangle-outline"),
            self.tr("Plot GSM Diagram (Blott && Pye, 2012)"))
        self.BP12_GSM_plot_selected_action = self.BP12_GSM_diagram_menu.addAction(
            self.tr("Plot Selected Samples"))
        self.BP12_GSM_plot_selected_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_GSM_diagram_chart, self.
                                    selections, False))
        self.BP12_GSM_append_selected_action = self.BP12_GSM_diagram_menu.addAction(
            self.tr("Append Selected Samples"))
        self.BP12_GSM_append_selected_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_GSM_diagram_chart, self.
                                    selections, True))
        self.BP12_GSM_plot_all_action = self.BP12_GSM_diagram_menu.addAction(
            self.tr("Plot All Samples"))
        self.BP12_GSM_plot_all_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_GSM_diagram_chart, self.__dataset
                                    .samples, False))
        self.BP12_GSM_append_all_action = self.BP12_GSM_diagram_menu.addAction(
            self.tr("Append All Samples"))
        self.BP12_GSM_append_all_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_GSM_diagram_chart, self.__dataset
                                    .samples, True))

        self.BP12_SSC_diagram_menu = self.menu.addMenu(
            qta.icon("mdi.triangle-outline"),
            self.tr("Plot SSC Diagram (Blott && Pye, 2012)"))
        self.BP12_SSC_plot_selected_action = self.BP12_SSC_diagram_menu.addAction(
            self.tr("Plot Selected Samples"))
        self.BP12_SSC_plot_selected_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_SSC_diagram_chart, self.
                                    selections, False))
        self.BP12_SSC_append_selected_action = self.BP12_SSC_diagram_menu.addAction(
            self.tr("Append Selected Samples"))
        self.BP12_SSC_append_selected_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_SSC_diagram_chart, self.
                                    selections, True))
        self.BP12_SSC_plot_all_action = self.BP12_SSC_diagram_menu.addAction(
            self.tr("Plot All Samples"))
        self.BP12_SSC_plot_all_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_SSC_diagram_chart, self.__dataset
                                    .samples, False))
        self.BP12_SSC_append_all_action = self.BP12_SSC_diagram_menu.addAction(
            self.tr("Append All Samples"))
        self.BP12_SSC_append_all_action.triggered.connect(
            lambda: self.plot_chart(self.BP12_SSC_diagram_chart, self.__dataset
                                    .samples, True))

        self.save_action = self.menu.addAction(qta.icon("mdi.microsoft-excel"),
                                               self.tr("Save Summary"))
        self.save_action.triggered.connect(self.on_save_clicked)
        self.data_table.customContextMenuRequested.connect(self.show_menu)

    def show_menu(self, pos):
        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)

    def load_dataset(self):
        self.load_dataset_dialog.show()

    def on_data_loaded(self, dataset: GrainSizeDataset):
        self.__dataset = dataset
        self.current_page_combo_box.clear()
        page_count, left = divmod(self.__dataset.n_samples, self.PAGE_ROWS)
        if left != 0:
            page_count += 1
        self.current_page_combo_box.addItems(
            [f"{self.tr('Page')} {i+1}" for i in range(page_count)])
        self.update_page(0)

    @property
    def is_geometric(self) -> bool:
        return self.geometric_checkbox.isChecked()

    def on_is_geometric_changed(self, state):
        if state == Qt.Checked:
            self.geometric_checkbox.setText(self.tr("Geometric"))
        else:
            self.geometric_checkbox.setText(self.tr("Logarithmic"))
        self.update_page(self.page_index)

    @property
    def is_FW57(self) -> bool:
        return self.FW57_checkbox.isChecked()

    def on_is_FW57_changed(self, state):
        if state == Qt.Checked:
            self.FW57_checkbox.setText(self.tr("Folk and Ward (1957) method"))
        else:
            self.FW57_checkbox.setText(self.tr("Method of statistic moments"))
        self.update_page(self.page_index)

    @property
    def proportion(self) -> str:
        index = self.proportion_combo_box.currentIndex()
        key, description = self.supported_proportions[index]
        return key, description

    @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 unit(self) -> str:
        return "μm" if self.is_geometric else "φ"

    def update_page(self, page_index: int):
        if self.__dataset is None:
            return

        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.2f}"
            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.__dataset.n_samples
        else:
            start, end = page_index * self.PAGE_ROWS, (page_index +
                                                       1) * self.PAGE_ROWS
        proportion_key, proportion_desciption = self.proportion
        col_names = [
            f"{self.tr('Mean')}[{self.unit}]",
            self.tr("Mean Desc."), f"{self.tr('Median')} [{self.unit}]",
            f"{self.tr('Modes')} [{self.unit}]",
            self.tr("STD (Sorting)"),
            self.tr("Sorting Desc."),
            self.tr("Skewness"),
            self.tr("Skew. Desc."),
            self.tr("Kurtosis"),
            self.tr("Kurt. Desc."),
            f"({proportion_desciption})\n{self.tr('Proportion')} [%]",
            self.tr("Group\n(Folk, 1954)"),
            self.tr("Group\nSymbol (Blott & Pye, 2012)"),
            self.tr("Group\n(Blott & Pye, 2012)")
        ]
        col_keys = [(True, "mean"), (True, "mean_description"),
                    (True, "median"), (True, "modes"), (True, "std"),
                    (True, "std_description"), (True, "skewness"),
                    (True, "skewness_description"), (True, "kurtosis"),
                    (True, "kurtosis_description"), (False, proportion_key),
                    (False, "group_Folk54"), (False, "group_BP12_symbol"),
                    (False, "group_BP12")]
        self.data_table.setRowCount(end - start)
        self.data_table.setColumnCount(len(col_names))
        self.data_table.setHorizontalHeaderLabels(col_names)
        self.data_table.setVerticalHeaderLabels(
            [sample.name for sample in self.__dataset.samples[start:end]])
        for row, sample in enumerate(self.__dataset.samples[start:end]):
            statistic = get_all_statistic(sample.classes_μm, sample.classes_φ,
                                          sample.distribution)
            if self.is_geometric:
                if self.is_FW57:
                    sub_key = "geometric_FW57"
                else:
                    sub_key = "geometric"
            else:
                if self.is_FW57:
                    sub_key = "logarithmic_FW57"
                else:
                    sub_key = "logarithmic"
            for col, (in_sub, key) in enumerate(col_keys):
                value = statistic[sub_key][key] if in_sub else statistic[key]
                if key == "modes":
                    write(row, col, ", ".join([f"{m:0.2f}" for m in value]))
                elif key[-11:] == "_proportion":
                    write(row, col,
                          ", ".join([f"{p*100:0.2f}" for p in value]))
                else:
                    write(row, col, value)

        self.data_table.resizeColumnsToContents()

    @property
    def selections(self):
        if self.__dataset.n_samples == 0:
            self.show_warning(self.tr("Dataset has not been loaded."))
            return []

        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()
        samples = [self.__dataset.samples[i] for i in indexes]
        return samples

    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 plot_chart(self, chart, samples, append):
        if len(samples) == 0:
            return
        chart.show_samples(samples, append=append)
        chart.show()

    def save_file(self, filename: str):
        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 one sheet:
            1. The sheet puts the statistic parameters and the classification groups of the samples.

            The statistic formulas are referred to Blott & Pye (2001)'s work.
            The classification of GSDs is referred to Folk (1957)'s and Blott & Pye (2012)'s scheme.

            References:
                1.Blott, S. J. & Pye, K. Particle size scales and classification of sediment types based on particle size distributions: Review and recommended procedures. Sedimentology 59, 2071–2096 (2012).
                2.Blott, S. J. & Pye, K. GRADISTAT: a grain-size distribution and statistics package for the analysis of unconsolidated sediments. Earth Surf. Process. Landforms 26, 1237–1248 (2001).
                3.Folk, R. L. The Distinction between Grain Size and Mineral Composition in Sedimentary-Rock Nomenclature. The Journal of Geology 62, 344–359 (1954).

            """.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("Parameters and Groups"))
        proportion_key, proportion_desciption = self.proportion
        col_names = [
            f"{self.tr('Mean')}[{self.unit}]",
            self.tr("Mean Desc."), f"{self.tr('Median')} [{self.unit}]",
            f"{self.tr('Modes')} [{self.unit}]",
            self.tr("STD (Sorting)"),
            self.tr("Sorting Desc."),
            self.tr("Skewness"),
            self.tr("Skew. Desc."),
            self.tr("Kurtosis"),
            self.tr("Kurt. Desc."),
            f"({proportion_desciption})\n{self.tr('Proportion')} [%]",
            self.tr("Group\n(Folk, 1954)"),
            self.tr("Group\nSymbol (Blott & Pye, 2012)"),
            self.tr("Group\n(Blott & Pye, 2012)")
        ]
        col_keys = [(True, "mean"), (True, "mean_description"),
                    (True, "median"), (True, "modes"), (True, "std"),
                    (True, "std_description"), (True, "skewness"),
                    (True, "skewness_description"), (True, "kurtosis"),
                    (True, "kurtosis_description"), (False, proportion_key),
                    (False, "group_Folk54"), (False, "group_BP12_symbol"),
                    (False, "group_BP12")]
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for col, moment_name in enumerate(col_names, 1):
            write(0, col, moment_name, style="header")
            if col in (2, 4, 6, 8, 10, 11, 12, 14):
                ws.column_dimensions[column_to_char(col)].width = 30
            else:
                ws.column_dimensions[column_to_char(col)].width = 16
        ws.column_dimensions[column_to_char(len(col_names))].width = 40
        for row, sample in enumerate(self.__dataset.samples, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, sample.name, style=style)
            statistic = get_all_statistic(sample.classes_μm, sample.classes_φ,
                                          sample.distribution)
            if self.is_geometric:
                if self.is_FW57:
                    sub_key = "geometric_FW57"
                else:
                    sub_key = "geometric"
            else:
                if self.is_FW57:
                    sub_key = "logarithmic_FW57"
                else:
                    sub_key = "logarithmic"
            for col, (in_sub, key) in enumerate(col_keys, 1):
                value = statistic[sub_key][key] if in_sub else statistic[key]
                if key == "modes":
                    write(row,
                          col,
                          ", ".join([f"{m:0.4f}" for m in value]),
                          style=style)
                elif key[-11:] == "_proportion":
                    write(row,
                          col,
                          ", ".join([f"{p*100:0.4f}" for p in value]),
                          style=style)
                else:
                    write(row, col, value, style=style)

        wb.save(filename)
        wb.close()

    def on_save_clicked(self):
        if self.__dataset is None or self.__dataset.n_samples == 0:
            self.show_warning(self.tr("Dataset has not been loaded."))
            return

        filename, _ = self.file_dialog.getSaveFileName(
            self, self.tr("Select Filename"), None, "Excel (*.xlsx)")
        if filename is None or filename == "":
            return

        try:
            self.save_file(filename)
            self.show_info(
                self.tr(
                    "The summary of this dataset has been saved to:\n    {0}").
                format(filename))
        except Exception as e:
            self.show_error(
                self.tr(
                    "Error raised while save summary to Excel file.\n    {0}").
                format(e.__str__()))
Пример #18
0
class SLUI(QWidget):
    """
    Widget containing all arcanas, descriptions of them, and the level/angles available.

    :param MainFrame mainframe: application mainframe
    :param QWidget op: parent widget
    """
    def __init__(self, mainframe, op):
        QWidget.__init__(self)
        self.mainframe = mainframe
        self.op = op

        # Initialized gui items...
        self.levelOM = None
        self.angleOM = None
        self.addAngB = None
        self.newAng = None
        self.go = None
        self.delang = None
        self.angs = None

        self.initUI()

    def initUI(self):
        """
        Initializes the GUI.
        Does a lot of stuff.
        """
        self.mainframe.setWindowTitle("Social Link Creator")
        self.grid = QGridLayout()
        self.setLayout(self.grid)

        arcanaList = json_reader.data_list("arcanas")

        self.arcSel = QComboBox(self)
        self.arcSel.addItem("Select Arcana")
        self.arcSel.activated.connect(self.showText)
        self.arcSel.addItems(arcanaList)
        self.arcSel.setCurrentIndex(0)
        self.grid.addWidget(self.arcSel, 1, 1)

        select = QPushButton(self, text="Select")
        select.clicked.connect(self.context)
        self.grid.addWidget(select, 2, 1)

        info = QPushButton(self, text="Info")
        info.clicked.connect(self.infoF)
        self.grid.addWidget(info, 3, 1)

        back = QPushButton(self, text="Back")
        back.clicked.connect(self.back)
        self.grid.addWidget(back, 4, 1)

        self.card = QLabel(self)
        defaultCard = QPixmap(json_reader.buildPath("int/cards/card.png"))
        self.card.setPixmap(defaultCard)
        self.card.setAlignment(Qt.AlignHCenter)
        self.grid.addWidget(self.card, 0, 0)

        self.text = QLabel(self, text="")
        self.text.setFixedSize(400, 250)
        self.text.setWordWrap(True)
        self.text.setAlignment(Qt.AlignHCenter)
        self.grid.addWidget(self.text, 1, 0, 4, 1)

    def infoF(self):
        """
        Enter the social link edit gui.
        """
        if self.arcSel.currentText() == "Select Arcana":
            return
        self.mainframe.changeState(
            LinkInfo(self.mainframe, self,
                     SocialLink(self.arcSel.currentText())))

    def context(self):
        """
        Once an arcana is selected, spawn this view to add the level/angle information as an additional
        widget.
        """
        self.destroyContext()
        if self.arcSel.currentText() == "Select Arcana":
            return
        levs = []
        for i in range(1, 11):
            levs.append("Level " + str(i))
        self.levelOM = QComboBox(self)
        self.levelOM.addItems(levs)
        self.levelOM.setCurrentIndex(0)
        self.levelOM.activated.connect(self.fetchangles)
        self.grid.addWidget(self.levelOM, 1, 2, 1, 2)

        self.angleOM = QComboBox(self)
        self.fetchangles()
        self.grid.addWidget(self.angleOM, 2, 2, 1, 2)

        self.addAngB = QPushButton(self, text="Add Angle")
        self.addAngB.clicked.connect(self.addAngle)
        self.grid.addWidget(self.addAngB, 3, 2)

        self.newAng = QLineEdit(self)
        self.newAng.setFixedSize(20, 20)
        self.grid.addWidget(self.newAng, 3, 3)

        self.go = QPushButton(self, text="Go")
        self.go.clicked.connect(self.begin)
        self.grid.addWidget(self.go, 5, 2, 1, 2)

    def fetchangles(self):
        """
        Fetch the angles at a certain level for the display.
        """
        try:
            self.delang.close()
        except:  #pylint: disable=bare-except
            print("Failed to close delang")
        self.angs = []
        try:
            tempLink = json_reader.readLink(str(self.arcSel.currentText()))
            for decon in tempLink["cutscenes"]:
                if str(decon)[:str(decon).index("_")] == \
                   self.levelOM.currentText()[str(self.levelOM.currentText()).index(" ") + 1:]:
                    self.angs.append("Angle " +
                                     str(decon)[str(decon).index("_") + 1:])
        except:  #pylint: disable=bare-except
            pass
        if self.angs:
            print("There are angles for this level")
            self.delang = QPushButton(self, text="Delete Angle")
            self.delang.clicked.connect(self.deleteangle)
            self.grid.addWidget(self.delang, 4, 2, 1, 2)
        else:
            self.angs.append("No angles")
        self.angleOM.clear()
        self.angleOM.addItems(self.angs)
        self.angleOM.setCurrentIndex(0)

    def addAngle(self):
        """
        Add a potential angle to this link/level.
        """
        try:
            (int)(self.newAng.text())
            if self.angs[0] == "No angles":
                self.angleOM.clear()
                self.delang = QPushButton(self, text="Delete Angle")
                self.delang.clicked.connect(self.deleteangle)
                self.grid.addWidget(self.delang, 4, 2, 1, 2)
            self.angleOM.addItem("Angle " + str(self.newAng.text()))
            self.angleOM.setCurrentIndex(self.angleOM.count() - 1)
            self.newAng.clear()
        except ValueError:
            popup("The Angle must be an integer", "Critical")

    def deleteangle(self):
        """
        Completely delete a certain level/angle combo. This will remove all info, the cutscene, etc.
        Dangerous.
        """
        if not popup(
                "WARNING!!!\n\nThis will COMPLETELY ERASE this cutscene. It is HIGHLY RECOMMENDED that "
                "you back up your data by going to the Support/Contact page and choose \"Export\".",
                "Warning"):
            return
        link = SocialLink(self.arcSel.currentText())
        print(link.cutscenes)
        key = self.levelOM.currentText()[self.levelOM.currentText().index(" ")+1:] + \
              "_" + \
              self.angleOM.currentText()[self.angleOM.currentText().index(" ")+1:]
        print(key)
        if key in link.cutscenes:
            link.cutscenes.pop(key)
            link.save()
        self.angleOM.removeItem(self.angleOM.currentIndex())
        if self.angleOM.count() == 0:
            self.angleOM.addItem("No angles")
            self.delang.close()
        print(link.cutscenes)
        print("Deleted")

    def showText(self):
        """
        Show the arcana's descriptive text upon selection.
        """
        temp = [self.arcSel.itemText(i) for i in range(self.arcSel.count())]
        if "Select Arcana" in temp:
            self.arcSel.removeItem(temp.index("Select Arcana"))
        self.text.setText(
            json_reader.readArcDesc(str(self.arcSel.currentText())))
        self.card.setPixmap(
            QPixmap(
                json_reader.buildPath("int/cards/" +
                                      str(self.arcSel.currentText()) +
                                      ".png")))
        self.destroyContext()

    def destroyContext(self):
        """
        Make sure all widgets are close when leaving the context.
        """
        try:
            self.levelOM.close()
            self.angleOM.close()
            self.addAngB.close()
            self.newAng.close()
            self.go.close()
            self.delang.close()
        except:  #pylint: disable=bare-except
            pass

    def begin(self):
        """
        Enter the social link cutscene editor.
        """
        if self.angleOM.currentText() == "No angles":
            popup(
                "An Angle must be selected.\nCreate angles by entering a number in the text box below and "
                "clicking \"Add Angle\"", "Critical")
            return
        enter_level = str(self.levelOM.currentText()
                          )[str(self.levelOM.currentText()).index(" ") + 1:]
        enter_angle = str(self.angleOM.currentText()
                          )[str(self.angleOM.currentText()).index(" ") + 1:]
        print("Entered SL creation mode for arcana " +
              str(self.arcSel.currentText()))
        self.mainframe.changeState(
            SLFrame(self.mainframe, self, str(self.arcSel.currentText()),
                    int(enter_level), int(enter_angle)))

    def back(self):
        """
        Return to the parent widget.
        """
        print("Returned to main screen")
        self.mainframe.changeState(self.op)
Пример #19
0
class EditCommentBankWindow(QMainWindow):
    def __init__(self, subject_name):
        QMainWindow.__init__(self)
        self.setWindowTitle("Edit Comment Bank: {} - {} {}".format(
            subject_name, config.APP_NAME, config.APP_VERSION))
        self.setMinimumWidth(1200)
        self.setStyleSheet(config.STYLESHEET)

        self.subject = could_try_harder.load(subject_name)
        self.saved_list = could_try_harder.get_saved_list()

        # Widgets
        self.intro_comment_label = QLabel("Introductory Comment:")
        self.intro_comment_label.setProperty("styleClass", "heading")
        self.intro_comment_textedit = QTextEdit()
        self.comment_bank_label = QLabel("Comment Bank")
        self.comment_bank_label.setProperty("styleClass", "heading")
        self.comment_bank_listwidget = QListWidget()
        self.placeholder_instructions_label = QLabel(
            config.PLACEHOLDER_INSTRUCTIONS)
        self.add_comment_label = QLabel("Add Comment:")
        self.add_comment_entry = QLineEdit()
        self.add_comment_button = QPushButton("Add")
        self.update_comment_label = QLabel("Update Comment:")
        self.update_comment_entry = QLineEdit()
        self.update_comment_button = QPushButton("Update")
        self.delete_comment_button = QPushButton("Delete Comment")
        self.import_comments_combo = QComboBox()
        self.import_comments_button = QPushButton("Import...")
        self.cancel_button = QPushButton("Cancel")
        self.save_button = QPushButton("Save")

        # Layout
        self.layout = QVBoxLayout()
        self.top_layout = QHBoxLayout()
        self.intro_comment_layout = QVBoxLayout()
        self.intro_comment_layout.addWidget(self.intro_comment_label)
        self.intro_comment_layout.addWidget(self.intro_comment_textedit)
        self.top_layout.addLayout(self.intro_comment_layout)
        self.top_layout.addWidget(self.placeholder_instructions_label)
        self.layout.addLayout(self.top_layout)
        self.middle_layout = QVBoxLayout()
        self.middle_layout.addWidget(self.comment_bank_label)
        self.middle_layout.addWidget(self.comment_bank_listwidget)
        self.comment_actions_layout = QHBoxLayout()
        self.comment_actions_layout.addWidget(self.delete_comment_button, 0,
                                              Qt.AlignLeft)
        self.comment_actions_layout.addWidget(self.import_comments_combo, 1,
                                              Qt.AlignRight)
        self.comment_actions_layout.addWidget(self.import_comments_button, 0,
                                              Qt.AlignRight)
        self.middle_layout.addLayout(self.comment_actions_layout)
        self.update_comment_layout = QGridLayout()
        self.update_comment_layout.addWidget(self.update_comment_label, 0, 0)
        self.update_comment_layout.addWidget(self.update_comment_entry, 0, 1)
        self.update_comment_layout.addWidget(self.update_comment_button, 0, 2)
        self.update_comment_layout.addWidget(self.add_comment_label, 1, 0)
        self.update_comment_layout.addWidget(self.add_comment_entry, 1, 1)
        self.update_comment_layout.addWidget(self.add_comment_button, 1, 2)
        self.middle_layout.addLayout(self.update_comment_layout)
        self.layout.addLayout(self.middle_layout)
        self.bottom_layout = QHBoxLayout()
        self.bottom_layout.addWidget(self.cancel_button, 0, Qt.AlignLeft)
        self.bottom_layout.addWidget(self.save_button, 0, Qt.AlignRight)
        self.layout.addLayout(self.bottom_layout)

        # Slot connections
        self.comment_bank_listwidget.itemSelectionChanged.connect(
            self.do_update_comment_bank_selection)
        self.import_comments_button.clicked.connect(self.do_import_comments)
        self.update_comment_button.clicked.connect(self.do_update_comment)
        self.update_comment_entry.returnPressed.connect(self.do_update_comment)
        self.add_comment_button.clicked.connect(self.do_add_comment)
        self.add_comment_entry.returnPressed.connect(self.do_add_comment)
        self.delete_comment_button.clicked.connect(self.do_delete_comment)
        self.cancel_button.clicked.connect(self.do_cancel)
        self.save_button.clicked.connect(self.do_save)

        # Initial UI update
        self.update_ui()

        self.widget = QWidget()
        self.widget.setLayout(self.layout)
        self.setCentralWidget(self.widget)

    def update_ui(self):
        self.update_import_comments_list()
        self.update_intro_comment()
        self.update_comment_bank()

    def update_import_comments_list(self):
        self.import_comments_combo.clear()
        self.import_comments_combo.insertItems(0, self.saved_list)

    def update_intro_comment(self):
        self.intro_comment_textedit.clear()
        self.intro_comment_textedit.insertPlainText(
            self.subject['intro_comment'])

    def update_comment_bank(self):
        self.comment_bank_listwidget.clear()
        self.comment_bank_listwidget.addItems(self.subject['comment_bank'])
        self.do_update_comment_bank_selection()

    @Slot()
    def do_import_comments(self):
        # TODO confirm dialog first
        confirm_msg = QMessageBox(self)
        confirm_msg.setWindowTitle("Confirm")
        confirm_msg.setText("This will override current comments.")
        confirm_msg.setInformativeText("Do you want to continue?")
        confirm_msg.setStandardButtons(QMessageBox.No | QMessageBox.Yes)
        confirm_msg.setDefaultButton(QMessageBox.Yes)
        confirm = confirm_msg.exec()
        if confirm == QMessageBox.Yes:
            if self.import_comments_combo.count() > 0:
                new_subject = could_try_harder.load(
                    self.import_comments_combo.currentText())
                if new_subject:
                    self.subject['intro_comment'] = new_subject[
                        'intro_comment']
                    self.subject['comment_bank'] = new_subject['comment_bank']
                    self.update_ui()
                else:
                    # TODO better error handling here
                    print('Tried to import empty subject.')
                    return
        return

    @Slot()
    def do_update_comment_bank_selection(self):
        if self.comment_bank_listwidget.selectedItems():
            state = True
        else:
            state = False
        self.delete_comment_button.setEnabled(state)
        self.update_comment_button.setEnabled(state)
        # Update the text in the update comment line edit
        self.update_comment_entry.clear()
        if self.comment_bank_listwidget.currentItem():
            self.update_comment_entry.insert(
                self.comment_bank_listwidget.currentItem().text())

    @Slot()
    def do_update_comment(self):
        if self.update_comment_entry.text():
            self.comment_bank_listwidget.currentItem().setText(
                could_try_harder.do_style(
                    self.update_comment_entry.text().strip()))
            self.do_update_comment_bank_selection()

    @Slot()
    def do_add_comment(self):
        if self.add_comment_entry.text():
            self.comment_bank_listwidget.addItem(
                could_try_harder.do_style(
                    self.add_comment_entry.text().strip()))
            self.add_comment_entry.clear()
            self.do_update_comment_bank_selection()

    @Slot()
    def do_delete_comment(self):
        self.comment_bank_listwidget.takeItem(
            self.comment_bank_listwidget.currentRow())
        self.do_update_comment_bank_selection()

    @Slot()
    def do_cancel(self):
        self.close()

    @Slot()
    def do_save(self):
        self.subject['intro_comment'] = could_try_harder.do_style(
            self.intro_comment_textedit.toPlainText().strip())
        self.subject['comment_bank'] = []
        for i in range(self.comment_bank_listwidget.count()):
            self.subject['comment_bank'].append(
                self.comment_bank_listwidget.item(i).text())
        if could_try_harder.save(self.subject):
            self.close()
        else:
            # TODO better error handling here
            print("Save failed.")
Пример #20
0
class QTgoLayoutGroupRow(QWidget):
    group_template_changed = Signal()

    def __init__(self, force_group: ForceGroup, group: TgoLayoutGroup) -> None:
        super().__init__()
        self.grid_layout = QGridLayout()
        self.setLayout(self.grid_layout)
        self.grid_layout.setColumnStretch(0, 100)
        self.amount_selector = QSpinBox()
        self.unit_selector = QComboBox()
        self.unit_selector.setMinimumWidth(250)
        self.group_selector = QCheckBox()

        # Add all possible units with the price
        for unit_type in force_group.unit_types_for_group(group):
            self.unit_selector.addItem(
                f"{unit_type.name} [${unit_type.price}M]",
                userData=(unit_type.dcs_unit_type, unit_type.price),
            )
        # Add all possible statics with price = 0
        for static_type in force_group.statics_for_group(group):
            self.unit_selector.addItem(f"{static_type} (Static)",
                                       userData=(static_type, 0))

        if self.unit_selector.count() == 0:
            raise LayoutException("No units available for the TgoLayoutGroup")

        self.unit_selector.adjustSize()
        self.unit_selector.setEnabled(self.unit_selector.count() > 1)
        self.grid_layout.addWidget(self.unit_selector,
                                   0,
                                   0,
                                   alignment=Qt.AlignRight)
        self.grid_layout.addWidget(self.amount_selector,
                                   0,
                                   1,
                                   alignment=Qt.AlignRight)

        dcs_unit_type, price = self.unit_selector.itemData(
            self.unit_selector.currentIndex())

        self.group_layout = QTgoLayoutGroup(group, dcs_unit_type,
                                            group.group_size, price)

        self.group_selector.setChecked(self.group_layout.enabled)
        self.group_selector.setEnabled(self.group_layout.layout.optional)

        self.amount_selector.setMinimum(1)
        self.amount_selector.setMaximum(self.group_layout.layout.max_size)
        self.amount_selector.setValue(self.group_layout.amount)
        self.amount_selector.setEnabled(self.group_layout.layout.max_size > 1)

        self.grid_layout.addWidget(self.group_selector,
                                   0,
                                   2,
                                   alignment=Qt.AlignRight)

        self.amount_selector.valueChanged.connect(self.on_group_changed)
        self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
        self.group_selector.stateChanged.connect(self.on_group_changed)

    def on_group_changed(self) -> None:
        self.group_layout.enabled = self.group_selector.isChecked()
        unit_type, price = self.unit_selector.itemData(
            self.unit_selector.currentIndex())
        self.group_layout.dcs_unit_type = unit_type
        self.group_layout.unit_price = price
        self.group_layout.amount = self.amount_selector.value()
        self.group_template_changed.emit()
Пример #21
0
class InputWindow(QDialog):
    """Window where user can enter new data into a table.
       It returns the data formatted into a dictionary.
       platforms: the platforms from the currently loaded
                  collection."""

    def __init__(self, platforms, parent=None):
        super(InputWindow, self).__init__(parent=parent)

        self.setContentsMargins(5, 5, 5, 5)

        self._platforms = ['1292 Advanced Programmable Video System',
                           '3DO',
                           'APF MP1000/Imagination Machine',
                           'Acorn 32-bit',
                           'Adventure Vision',
                           'Alice 32/90',
                           'Altair 680',
                           'Altair 8800',
                           'Amazon Alexa',
                           'Amiga',
                           'Amiga CD32',
                           'Amstrad CPC',
                           'Amstrad PCW',
                           'Android',
                           'Apple I',
                           'Apple II',
                           'Apple IIgs',
                           'Arcade',
                           'Arcadia 2001',
                           'Arduboy',
                           'Astral 2000',
                           'Atari 2600',
                           'Atari 5200',
                           'Atari 7800',
                           'Atari 8-bit',
                           'Atari ST',
                           'Atom',
                           'BBC Micro',
                           'BREW',
                           'Bally Astrocade',
                           'BeOS',
                           'BlackBerry',
                           'Blu-ray Disc Player',
                           'Browser',
                           'Bubble',
                           'CD-i',
                           'CDTV',
                           'CP/M',
                           'Camputers Lynx',
                           'Casio Loopy',
                           'Casio PV-1000',
                           'ClickStart',
                           'Coleco Adam',
                           'ColecoVision',
                           'Colour Genie',
                           'Commodore 128',
                           'Commodore 16, Plus/4',
                           'Commodore 64',
                           'Commodore PET/CBM',
                           'Compal 80',
                           'Compucolor I',
                           'Compucolor II',
                           'Compucorp Programmable Calculator',
                           'CreatiVision',
                           'DOS',
                           'DVD Player',
                           'Dedicated console',
                           'Dedicated handheld',
                           'Didj',
                           'DoJa',
                           'Dragon 32/64',
                           'ECD Micromind',
                           'Electron',
                           'Enterprise',
                           'Epoch Cassette Vision',
                           'Epoch Game Pocket Computer',
                           'Epoch Super Cassette Vision',
                           'ExEn',
                           'Exelvision',
                           'Exidy Sorcerer',
                           'Fairchild Channel F',
                           'Famicom Disk System',
                           'FM Towns',
                           'FM-7',
                           'FRED/COSMAC',
                           'Fire OS',
                           'Freebox',
                           'GIMINI',
                           'GNEX',
                           'GP2X',
                           'GP2X Wiz',
                           'GP32',
                           'GVM',
                           'Galaksija',
                           'Game Boy',
                           'Game Boy Advance',
                           'Game Boy Color',
                           'Game Gear',
                           'Game Wave',
                           'Game.Com',
                           'GameCube',
                           'GameStick',
                           'Genesis',
                           'Gizmondo',
                           'Glulx',
                           'HD DVD Player',
                           'HP 9800',
                           'HP Programmable Calculator',
                           'Heath/Zenith H8/H89',
                           'Heathkit H11',
                           'Hitachi S1',
                           'Hugo',
                           'HyperScan',
                           'IBM 5100',
                           'Ideal-Computer',
                           'Intel 8008',
                           'Intel 8080',
                           'Intellivision',
                           'Interton Video 2000',
                           'J2ME',
                           'Jaguar',
                           'Jolt',
                           'Jupiter Ace',
                           'KIM-1',
                           'Kindle Classic',
                           'Laser 200',
                           'LaserActive',
                           'LeapFrog Explorer',
                           'LeapTV',
                           'Leapster',
                           'Linux',
                           'Lynx',
                           'MOS Technology 6502',
                           'MRE',
                           'MSX',
                           'MSX 2',
                           'Macintosh',
                           'Maemo',
                           'Mainframe',
                           'Mattel Aquarius',
                           'MeeGo',
                           'Mega Drive',
                           'Memotech MTX',
                           'Microbee',
                           'Microtan 65',
                           'Microvision',
                           'Mophun',
                           'Motorola 6800',
                           'Motorola 68k',
                           'N-Gage',
                           'N-Gage (service)',
                           'NES',
                           'Nascom',
                           'Neo Geo AES',
                           'Neo Geo CD',
                           'Neo Geo MVS',
                           'Neo Geo Pocket',
                           'Neo Geo Pocket Color',
                           'Neo Geo X',
                           'New Nintendo 3DS',
                           'NewBrain',
                           'Newton',
                           'Nintendo 3DS',
                           'Nintendo 64',
                           'Nintendo DS',
                           'Nintendo DSi',
                           'Nintendo Switch',
                           'North Star',
                           'Noval 760',
                           'Nuon',
                           'OS/2',
                           'Oculus Go',
                           'Oculus Quest',
                           'Odyssey',
                           'Odyssey 2',
                           'Ohio Scientific',
                           'OnLive',
                           'Orao',
                           'Oric',
                           'Ouya',
                           'PC Booter',
                           'PC-6001',
                           'PC-8000',
                           'PC-88',
                           'PC-98',
                           'PC-FX',
                           'PS Vita',
                           'PSP',
                           'Palm OS',
                           'Philips VG 5000',
                           'Photo CD',
                           'Pippin',
                           'PlayStation',
                           'PlayStation 2',
                           'PlayStation 3',
                           'PlayStation 4',
                           'Playdia',
                           'Pokémon Mini',
                           'Pokitto',
                           'Poly-88',
                           'RCA Studio II',
                           'Roku',
                           'SAM Coupé',
                           'SC/MP',
                           'SD-200/270/290',
                           'SEGA 32X',
                           'SEGA CD',
                           'SEGA Dreamcast',
                           'SEGA Master System',
                           'SEGA Pico',
                           'SEGA Saturn',
                           'SG-1000',
                           'SK-VM',
                           'SMC-777',
                           'SNES',
                           'SRI-500/1000',
                           'SWTPC 6800',
                           'Sharp MZ-80B/2000/2500',
                           'Sharp MZ-80K/700/800/1500',
                           'Sharp X1',
                           'Sharp X68000',
                           'Sharp Zaurus',
                           'Signetics 2650',
                           'Sinclair QL',
                           'Sol-20',
                           'Sord M5',
                           'Spectravideo',
                           'Stadia',
                           "Super A'can",
                           'SuperGrafx',
                           'Supervision',
                           'Symbian',
                           'TADS',
                           'TI Programmable Calculator',
                           'TI-99/4A',
                           'TIM',
                           'TRS-80',
                           'TRS-80 CoCo',
                           'TRS-80 MC-10',
                           'Taito X-55',
                           'Tatung Einstein',
                           'Tektronix 4050',
                           'Tele-Spiel ES-2201',
                           'Telstar Arcade',
                           'Terminal',
                           'Thomson MO',
                           'Thomson TO',
                           'Tiki 100',
                           'Timex Sinclair 2068',
                           'Tizen',
                           'Tomahawk F1',
                           'Tomy Tutor',
                           'TurboGrafx CD',
                           'TurboGrafx-16',
                           'V.Flash',
                           'V.Smile',
                           'VIC-20',
                           'VIS',
                           'Vectrex',
                           'VideoBrain',
                           'Videopac+ G7400',
                           'Virtual Boy',
                           'WIPI',
                           'Wang 2200',
                           'Wii',
                           'Wii U',
                           'Windows',
                           'Windows 3.x',
                           'Windows Apps',
                           'Windows Mobile',
                           'Windows Phone',
                           'WonderSwan',
                           'WonderSwan Color',
                           'XaviXPORT',
                           'Xbox',
                           'Xbox 360',
                           'Xbox One',
                           'Xerox Alto',
                           'Z-machine',
                           'ZX Spectrum',
                           'ZX Spectrum Next',
                           'ZX80',
                           'ZX81',
                           'Zeebo',
                           'Zilog Z80',
                           'Zilog Z8000',
                           'Zodiac',
                           'Zune',
                           'bada',
                           'digiBlast',
                           'iPad',
                           'iPhone',
                           'iPod Classic',
                           'tvOS',
                           'watchOS',
                           'webOS']

        # Add any platforms in user's collection that doesn't exist in the platform list
        for platform in platforms:
            if platform not in self._platforms:
                self._platforms.append(platform)
        self._platforms.sort()

        # For holding the internal platforms column's data
        self._platformsData = ""

        self._dataTypes = ["Game", "Console", "Accessory"]
        self._dataTypeLabel = QLabel("Type\t ")
        self._dataType = QComboBox()
        self._dataType.addItems(self._dataTypes)
        self._dataType.currentIndexChanged.connect(self._changeWidgets)

        self._nameLabel = QLabel("Name\t ")
        self._name = QLineEdit()
        self._name.textChanged.connect(self._changeWidgets)

        self._platformLabel = QLabel("Platform\t ")
        self._platform = QComboBox()
        self._platform.view().setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        self._platform.addItems(["", "(New platform)"])
        self._platform.addItems(self._platforms)
        self._platform.currentIndexChanged.connect(self._addPlatform)

        self._autofillButton = QPushButton("Autofill")
        self._autofillButton.clicked.connect(self._autofill)
        self._autofillButton.setEnabled(False)

        self._regionLabel = QLabel("Region\t ")
        self._region = QComboBox()
        self._region.addItems(["NTSC (JP)", "NTSC (NA)", "PAL"])

        self._countryLabel = QLabel("Country\t ")
        self._countryLabel.setEnabled(True)
        self._country = QLineEdit()
        self._country.setEnabled(True)

        self._publisherLabel = QLabel("Publisher\t")
        self._publisherLabel.setEnabled(True)
        self._publisher = QLineEdit()
        self._publisher.setEnabled(True)

        self._developerLabel = QLabel("Developer")
        self._developer = QLineEdit()
        self._developer.setEnabled(True)

        self._codeLabel = QLabel("Code\t ")
        self._code = QLineEdit()

        self._itemLabel = QLabel("Game")
        self._item = QCheckBox()

        self._boxLabel = QLabel("Box")
        self._box = QCheckBox()

        self._manualLabel = QLabel("Manual")
        self._manual = QCheckBox()

        self._yearLabel = QLabel("Year\t ")
        self._year = QLineEdit()

        self._genreLabel = QLabel("Genre\t ")
        self._genre = QLineEdit()

        self._paidLabel = QLabel("Paid price ")
        self._paid = QLineEdit("$0")

        self._commentLabel = QLabel("Comment")
        self._comment = QLineEdit()

        self._okButton = QPushButton()
        self._okButton.setText("OK")
        self._okButton.setMaximumSize(self._okButton.sizeHint())
        self._okButton.clicked.connect(self.accept)
        self._cnclButton = QPushButton()
        self._cnclButton.setText("Cancel")
        self._cnclButton.setMaximumSize(self._cnclButton.sizeHint())
        self._cnclButton.clicked.connect(self.reject)

        self._vbox = QVBoxLayout()
        self._vbox.addStretch()
        self._hboxType = QHBoxLayout()
        self._hboxType.addStretch()
        self._hboxName = QHBoxLayout()
        self._hboxName.addStretch()
        self._hboxPlatform = QHBoxLayout()
        self._hboxRegion = QHBoxLayout()
        self._hboxRegion.addStretch()
        self._hboxCode = QHBoxLayout()
        self._hboxCode.addStretch()
        self._hboxCountry = QHBoxLayout()
        self._hboxCountry.addStretch()
        self._hboxPublisher = QHBoxLayout()
        self._hboxDeveloper = QHBoxLayout()
        self._hboxBoxMan = QHBoxLayout()
        self._hboxYear = QHBoxLayout()
        self._hboxGenre = QHBoxLayout()
        self._hboxPaid = QHBoxLayout()
        self._hboxComment = QHBoxLayout()
        self._hboxComment.addStretch()
        self._hboxBtn = QHBoxLayout()
        self._hboxBtn.addStretch()

        self._hboxType.addWidget(self._dataTypeLabel, 0)
        self._hboxType.addWidget(self._dataType, 1)
        self._hboxName.addWidget(self._nameLabel, 0)
        self._hboxName.addWidget(self._name, 1)
        self._hboxPlatform.addWidget(self._platformLabel, 0)
        self._hboxPlatform.addWidget(self._platform, 1)
        self._hboxRegion.addWidget(self._regionLabel, 0)
        self._hboxRegion.addWidget(self._region, 1)
        self._hboxCountry.addWidget(self._countryLabel, 0)
        self._hboxCountry.addWidget(self._country, 1)
        self._hboxPublisher.addWidget(self._publisherLabel, 0)
        self._hboxPublisher.addSpacing(5)
        self._hboxPublisher.addWidget(self._publisher, 1)
        self._hboxDeveloper.addWidget(self._developerLabel, 0)
        self._hboxDeveloper.addWidget(self._developer, 1)
        self._hboxCode.addWidget(self._codeLabel, 0)
        self._hboxCode.addWidget(self._code, 1)
        self._hboxYear.addWidget(self._yearLabel, 0)
        self._hboxYear.addWidget(self._year, 1)
        self._hboxGenre.addWidget(self._genreLabel, 0)
        self._hboxGenre.addWidget(self._genre, 1)
        self._hboxPaid.addWidget(self._paidLabel, 0)
        self._hboxPaid.addWidget(self._paid, 1)
        self._hboxComment.addWidget(self._commentLabel, 0)
        self._hboxComment.addSpacing(2)
        self._hboxComment.addWidget(self._comment, 1)
        self._hboxBoxMan.addStretch(10)
        self._hboxBoxMan.addWidget(self._itemLabel, 0)
        self._hboxBoxMan.addWidget(self._item, 1)
        self._hboxBoxMan.addStretch(5)
        self._hboxBoxMan.addWidget(self._boxLabel, 2)
        self._hboxBoxMan.addWidget(self._box, 3)
        self._hboxBoxMan.addStretch(5)
        self._hboxBoxMan.addWidget(self._manualLabel, 4)
        self._hboxBoxMan.addWidget(self._manual, 5)
        self._hboxBoxMan.addStretch(10)
        self._hboxBtn.addWidget(self._autofillButton, 0, Qt.AlignLeft)
        self._hboxBtn.addStretch(10)
        self._hboxBtn.addWidget(self._okButton, 1)
        self._hboxBtn.addWidget(self._cnclButton, 2)

        self._vbox.addLayout(self._hboxType, 0)
        self._vbox.addLayout(self._hboxName, 1)
        self._vbox.addLayout(self._hboxPlatform, 2)
        self._vbox.addLayout(self._hboxRegion, 3)
        self._vbox.addLayout(self._hboxPublisher, 4)
        self._vbox.addLayout(self._hboxDeveloper, 5)
        self._vbox.addLayout(self._hboxCountry, 6)
        self._vbox.addLayout(self._hboxCode, 7)
        self._vbox.addLayout(self._hboxYear, 8)
        self._vbox.addLayout(self._hboxGenre, 9)
        self._vbox.addLayout(self._hboxPaid, 10)
        self._vbox.addLayout(self._hboxComment, 11)
        self._vbox.addLayout(self._hboxBoxMan, 12)
        self._vbox.addLayout(self._hboxBtn, 13)

        self.setLayout(self._vbox)

        self.setWindowTitle("Add to collection")
        self.setFixedSize(QSize(500, 340))
        self._center()

    def _addPlatform(self):
        if (self._platform.currentText() not in ("", "(New platform)") and
                self._name.text() != "" and not self._name.text().isspace()):
            self._autofillButton.setEnabled(True)
        else:
            self._autofillButton.setEnabled(False)

        if self._platform.currentText() == "(New platform)":
            while True:
                platform, ok = QInputDialog.getText(self, "Add platform", "Platform name:")
                if ok:
                    if platform == "" or platform.isspace():
                        self._displayMsgBox(0)
                    else:
                        lastIndex = self._platform.count()
                        self._platform.addItem(platform)
                        self._platform.setCurrentIndex(lastIndex)
                        break
                else:
                    break

    def _autofill(self):
        name = self._name.text()
        platform = self._platform.currentText()
        country = self._country.text()
        region = self._region.currentText()

        if name == "" or platform == "" or name.isspace() or platform.isspace():
            self._displayMsgBox(1)
        else:
            # Fill in missing info
            info = getMobyRelease(name, platform, region, country)
            if info["publisher"] == "":
                self._displayMsgBox(2)
                return

            publisher = info["publisher"]
            developer = info["developer"]
            platforms = info["platforms"]
            genre = info["genre"]
            code = info["code"]
            year = info["year"]

            if year == "" and code == "" and country != "":
                # Can't find release for country
                self._displayMsgBox(3)
            elif year == "" and code == "":
                # Can't find release at all
                self._displayMsgBox(4)
            else:
                self._year.setText(year)
                self._code.setText(code)
                self._genre.setText(genre)
                self._publisher.setText(publisher)
                self._developer.setText(developer)
                self._platformsData = platforms

    def _center(self):
        """Centers window on screen"""

        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def _changeWidgets(self):
        """Changes the label widgets based on what type of data is being entered"""

        if self._dataType.currentIndex() == 0:
            self._codeLabel.setEnabled(True)
            self._code.setEnabled(True)
            self._countryLabel.setEnabled(True)
            self._country.setEnabled(True)
            self._codeLabel.setText("Code\t ")
            self._itemLabel.setText("Game")
            self._genreLabel.setEnabled(True)
            self._genre.setEnabled(True)
            if (self._name.text() != "" and not self._name.text().isspace() and
                    self._platform.currentText() not in ("", "(New platform)")):
                self._autofillButton.setEnabled(True)
            else:
                self._autofillButton.setEnabled(False)
        elif self._dataType.currentIndex() == 1:
            self._codeLabel.setEnabled(True)
            self._code.setEnabled(True)
            self._countryLabel.setEnabled(True)
            self._country.setEnabled(True)
            self._codeLabel.setText("Serial No\t ")
            self._itemLabel.setText("Console")
            self._genreLabel.setEnabled(False)
            self._genre.setEnabled(False)
            self._autofillButton.setEnabled(False)
        elif self._dataType.currentIndex() == 2:
            self._countryLabel.setEnabled(True)
            self._country.setEnabled(True)
            self._codeLabel.setEnabled(False)
            self._code.setEnabled(False)
            self._itemLabel.setText("Accessory")
            self._genreLabel.setEnabled(False)
            self._genre.setEnabled(False)
            self._autofillButton.setEnabled(False)

    def _displayMsgBox(self, value: int, level=QMessageBox.Warning):
        titles = ["Invalid platform",
                  "Missing data",
                  "No title found",
                  "No info found for country",
                  "No info found"]
        messages = ["Can't add empty string or whitespace.",
                    "Please enter both a name and a platform.",
                    "Couldn't find any info on that title. Is the spelling and platform correct?",
                    "Couldn't find any info for that country. Is the region correct?",
                    "Sorry, couldn't find any info about this title."]

        msgBox = QMessageBox()
        msgBox.setIcon(level)
        msgBox.setWindowTitle(titles[value])
        msgBox.setText("<h2>" + titles[value] + "</h2>")
        msgBox.setInformativeText(messages[value])
        msgBox.exec_()

    def returnData(self):
        data = None

        if self._dataType.currentIndex() == 0:
            data = {"platform":     self._platform.currentText(),
                    "name":         self._name.text(),
                    "region":       self._region.currentText(),
                    "code":         self._code.text(),
                    "game":         "Yes" if self._item.isChecked() else "No",
                    "box":          "Yes" if self._box.isChecked() else "No",
                    "manual":       "Yes" if self._manual.isChecked() else "No",
                    "year":         self._year.text(),
                    "genre":        self._genre.text(),
                    "price":        ",".join((self._paid.text(), "$0", "$0", "$0")),
                    "comment":      self._comment.text(),
                    "publisher":    self._publisher.text(),
                    "developer":    self._developer.text(),
                    "platforms":    self._platformsData
                    }

        elif self._dataType.currentIndex() == 1:
            data = {"platform":         self._platform.currentText(),
                    "name":             self._name.text(),
                    "region":           self._region.currentText(),
                    "country":          self._country.text(),
                    "serial number":    self._code.text(),
                    "console":          "Yes" if self._item.isChecked() else "No",
                    "box":              "Yes" if self._box.isChecked() else "No",
                    "manual":           "Yes" if self._manual.isChecked() else "No",
                    "year":             self._year.text(),
                    "price":            ",".join((self._paid.text(), "$0", "$0", "$0")),
                    "comment":          self._comment.text()
                    }

        elif self._dataType.currentIndex() == 2:
            data = {"platform":     self._platform.currentText(),
                    "name":         self._name.text(),
                    "region":       self._region.currentText(),
                    "country":      self._country.text(),
                    "accessory":    "Yes" if self._item.isChecked() else "No",
                    "box":          "Yes" if self._box.isChecked() else "No",
                    "manual":       "Yes" if self._manual.isChecked() else "No",
                    "year":         self._year.text(),
                    "price":        ",".join((self._paid.text(), "$0", "$0", "$0")),
                    "comment":      self._comment.text()
                    }

        return data
Пример #22
0
class ReferenceResultViewer(QDialog):
    PAGE_ROWS = 20
    logger = logging.getLogger("root.QGrain.ui.ReferenceResultViewer")

    def __init__(self, parent=None):
        super().__init__(parent=parent, f=Qt.Window)
        self.setWindowTitle(self.tr("SSU Reference Result Viewer"))
        self.__fitting_results = []
        self.__reference_map = {}
        self.retry_tasks = {}
        self.init_ui()
        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.update_page_list()
        self.update_page(self.page_index)

        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.normal_msg = QMessageBox(self)

    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.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.unmark_action = self.menu.addAction(
            qta.icon("mdi.do-not-disturb"), self.tr("Unmark Selection(s)"))
        self.unmark_action.triggered.connect(self.unmark_selections)
        self.remove_action = self.menu.addAction(
            qta.icon("fa.remove"), self.tr("Remove Selection(s)"))
        self.remove_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.load_dump_action = self.menu.addAction(
            qta.icon("fa.database"), self.tr("Load Binary Dump"))
        self.load_dump_action.triggered.connect(
            lambda: self.load_dump(mark_ref=True))
        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.data_table.customContextMenuRequested.connect(self.show_menu)

    def show_menu(self, pos):
        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(8)
        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"),
            self.tr("Is 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"))
            is_ref = result.uuid in self.__reference_map
            write(row, 7, self.tr("Yes") if is_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_results(self, results: typing.List[SSUResult]):
        for result in results:
            self.__reference_map[result.uuid] = result

        self.update_page(self.page_index)

    def unmark_results(self, results: typing.List[SSUResult]):
        for result in results:
            if result.uuid in self.__reference_map:
                self.__reference_map.pop(result.uuid)

        self.update_page(self.page_index)

    def add_references(self, results: typing.List[SSUResult]):
        self.add_results(results)
        self.mark_results(results)

    def mark_selections(self):
        results = [
            self.__fitting_results[selection] for selection in self.selections
        ]
        self.mark_results(results)

    def unmark_selections(self):
        results = [
            self.__fitting_results[selection] for selection in self.selections
        ]
        self.unmark_results(results)

    def remove_results(self, indexes):
        results = []
        for i in reversed(indexes):
            res = self.__fitting_results.pop(i)
            results.append(res)
        self.unmark_results(results)
        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, mark_ref=False):
        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)
            valid = True
            if isinstance(results, list):
                for result in results:
                    if not isinstance(result, SSUResult):
                        valid = False
                        break
            else:
                valid = False

            if valid:
                self.add_results(results)
                if mark_ref:
                    self.mark_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 find_similar(self, target: GrainSizeSample,
                     ref_results: typing.List[SSUResult]):
        assert len(ref_results) != 0
        # sample_moments = logarithmic(sample.classes_φ, sample.distribution)
        # keys_to_check = ["mean", "std", "skewness", "kurtosis"]

        start_time = time.time()
        from scipy.interpolate import interp1d
        min_distance = 1e100
        min_result = None
        trans_func = interp1d(target.classes_φ,
                              target.distribution,
                              bounds_error=False,
                              fill_value=0.0)
        for result in ref_results:
            # TODO: To scale the classes of result to that of sample
            # use moments to calculate? MOMENTS MAY NOT BE PERFECT, MAY IGNORE THE MINOR DIFFERENCE
            # result_moments = logarithmic(result.classes_φ, result.distribution)
            # distance = sum([(sample_moments[key]-result_moments[key])**2 for key in keys_to_check])
            trans_dist = trans_func(result.classes_φ)
            distance = self.distance_func(result.distribution, trans_dist)

            if distance < min_distance:
                min_distance = distance
                min_result = result

        self.logger.debug(
            f"It took {time.time()-start_time:0.4f} s to query the reference from {len(ref_results)} results."
        )
        return min_result

    def query_reference(self, sample: GrainSizeSample):
        if len(self.__reference_map) == 0:
            self.logger.debug("No result is marked as reference.")
            return None
        return self.find_similar(sample, self.__reference_map.values())
Пример #23
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()
class guiMain(QMainWindow):
    def __init__(self, bk, prefs):
        super(guiMain, self).__init__()
        self.taglist = taglist
        # Edit Plugin container object
        self.bk = bk

        # Handy prefs groupings
        self.gui_prefs = prefs['gui_selections']
        self.misc_prefs = prefs['miscellaneous_settings']
        self.update_prefs = prefs['update_settings']
        self.combobox_values = prefs['combobox_values']

        self._ok_to_close = False
        # Check online github files for newer version
        self.update, self.newversion = self.check_for_update()
        self.setup_ui()

    def setup_ui(self):
        app = QApplication.instance()
        p = app.palette()
        link_color = p.color(p.Active, p.Link).name()

        DELETE_STR = _t('guiMain', 'Delete')
        MODIFY_STR = _t('guiMain', 'Modify')
        self.NO_ATTRIB_STR = _t('guiMain', 'No attributes (naked tag)')
        self.NO_CHANGE_STR = _t('guiMain', 'No change')
        self.setWindowTitle(_t('guiMain', 'Tag Mechanic'))

        configAct = QAction(_t('guiMain', '&Config'), self)
        configAct.triggered.connect(self.showConfig)

        menubar = self.menuBar()
        fileMenu = menubar.addMenu(_t('guiMain', '&Edit'))
        fileMenu.addAction(configAct)

        layout = QVBoxLayout()

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        if self.update:
            update_layout = QHBoxLayout()
            layout.addLayout(update_layout)
            self.label = QLabel()
            self.label.setText(
                _t('guiMain', 'Plugin Update Available') + ' ' +
                str(self.newversion))
            self.label.setStyleSheet(
                'QLabel {{color: {};}}'.format(link_color))
            update_layout.addWidget(self.label)

        action_layout = QHBoxLayout()
        layout.addLayout(action_layout)
        label = QLabel(_t('guiMain', 'Action type:'), self)
        action_layout.addWidget(label)
        self.action_combo = QComboBox()
        action_layout.addWidget(self.action_combo)
        self.action_combo.addItems([DELETE_STR, MODIFY_STR])
        self.action_combo.setCurrentIndex(self.gui_prefs['action'])
        self.action_combo.currentIndexChanged.connect(self.update_gui)

        tag_layout = QHBoxLayout()
        layout.addLayout(tag_layout)
        label = QLabel(_t('guiMain', 'Tag name:'), self)
        tag_layout.addWidget(label)
        self.tag_combo = QComboBox()
        tag_layout.addWidget(self.tag_combo)
        self.tag_combo.addItems(self.taglist)
        self.tag_combo.setCurrentIndex(self.gui_prefs['tag'])
        self.tag_combo.currentIndexChanged.connect(self.update_gui)

        attr_layout = QHBoxLayout()
        layout.addLayout(attr_layout)
        label = QLabel(_t('guiMain', 'Having the attribute:'), self)
        attr_layout.addWidget(label)
        self.attr_combo = QComboBox()
        attr_layout.addWidget(self.attr_combo)
        self.attr_combo.addItems(self.combobox_values['attrs'])
        self.attr_combo.addItem(self.NO_ATTRIB_STR)
        self.attr_combo.setCurrentIndex(self.gui_prefs['attrs'])
        self.attr_combo.currentIndexChanged.connect(self.update_gui)

        srch_layout = QHBoxLayout()
        layout.addLayout(srch_layout)
        label = QLabel(_t('guiMain', 'Whose value is (no quotes):'), self)
        srch_layout.addWidget(label)
        self.srch_txt = QLineEdit('', self)
        srch_layout.addWidget(self.srch_txt)
        self.srch_method = QCheckBox(_t('guiMain', 'Regex'), self)
        srch_layout.addWidget(self.srch_method)

        newtag_layout = QHBoxLayout()
        layout.addLayout(newtag_layout)
        label = QLabel(_t('guiMain', 'Change tag to:'), self)
        newtag_layout.addWidget(label)
        self.newtag_combo = QComboBox()
        newtag_layout.addWidget(self.newtag_combo)

        self.newtag_combo.addItem(self.NO_CHANGE_STR)
        self.newtag_combo.addItems(self.combobox_values['{}_changes'.format(
            str(self.tag_combo.currentText()))])

        if self.action_combo.currentIndex() == 0:
            self.newtag_combo.setDisabled(True)

        newattr_layout = QVBoxLayout()
        layout.addLayout(newattr_layout)
        label = QLabel(
            _t('guiMain', 'New attribute string to insert (entire):'), self)
        newattr_layout.addWidget(label)
        self.newattr_txt = QLineEdit('', self)
        newattr_layout.addWidget(self.newattr_txt)
        self.copy_attr = QCheckBox(
            _t('guiMain', 'Copy existing attribute string'), self)
        self.copy_attr.stateChanged.connect(self.update_txt_box)
        newattr_layout.addWidget(self.copy_attr)
        if self.action_combo.currentIndex() == 0:
            self.copy_attr.setDisabled(True)
            self.newattr_txt.setDisabled(True)

        layout.addSpacing(10)
        self.text_panel = QTextEdit()
        self.text_panel.setReadOnly(True)
        layout.addWidget(self.text_panel)

        layout.addSpacing(10)
        button_layout = QHBoxLayout()
        layout.addLayout(button_layout)
        self.process_button = QPushButton(_t('guiMain', 'Process'), self)
        self.process_button.setToolTip('<p>{}'.format(
            _t('guiMain', 'Process selected files with current criteria')))
        self.process_button.clicked.connect(self._process_clicked)
        button_layout.addWidget(self.process_button)

        self.abort_button = QPushButton(_t('guiMain', 'Abort Changes'), self)
        self.abort_button.setToolTip('<p>{}'.format(
            _t('guiMain', 'Make no changes and exit')))
        self.abort_button.clicked.connect(self._abort_clicked)
        self.abort_button.setDisabled(True)
        button_layout.addWidget(self.abort_button)

        self.quit_button = QPushButton(_t('guiMain', 'Quit'), self)
        self.quit_button.setToolTip('<p>{}'.format(
            _t('guiMain', 'Quit with no changes')))
        self.quit_button.clicked.connect(self._quit_clicked)
        button_layout.addWidget(self.quit_button)

        if self.misc_prefs['windowGeometry'] is not None:
            try:
                self.restoreGeometry(
                    QByteArray.fromHex(
                        self.misc_prefs['windowGeometry'].encode('ascii')))
            except Exception:
                pass
        self.show()

    def update_gui(self):
        if self.attr_combo.currentIndex() == self.attr_combo.count() - 1:
            self.srch_txt.clear()
            self.srch_txt.setDisabled(True)
            self.srch_method.setChecked(False)
            self.srch_method.setDisabled(True)
        else:
            self.srch_txt.setDisabled(False)
            self.srch_method.setDisabled(False)

        self.newtag_combo.clear()
        self.newtag_combo.addItem(self.NO_CHANGE_STR)
        self.newtag_combo.addItems(self.combobox_values['{}_changes'.format(
            str(self.tag_combo.currentText()))])

        if self.action_combo.currentIndex() == 0:
            self.newtag_combo.setCurrentIndex(0)
            self.newtag_combo.setDisabled(True)
            self.newattr_txt.clear()
            self.newattr_txt.setDisabled(True)
            self.copy_attr.setChecked(False)
            self.copy_attr.setDisabled(True)
        else:
            self.newtag_combo.setDisabled(False)
            self.newattr_txt.setDisabled(False)
            self.copy_attr.setDisabled(False)

        self.update_txt_box()

    def update_txt_box(self):
        if self.copy_attr.isChecked() or not self.copy_attr.isEnabled():
            self.newattr_txt.clear()
            self.newattr_txt.setDisabled(True)
        else:
            self.newattr_txt.setDisabled(False)

    def refresh_attr_values(self):
        self.attr_combo.clear()
        self.attr_combo.addItems(self.combobox_values['attrs'])
        self.attr_combo.addItem(self.NO_ATTRIB_STR)

    def _process_clicked(self):
        criteria = {}
        global PROCESSED
        criteria['tag'] = str(self.tag_combo.currentText())
        if self.action_combo.currentIndex() == 0:
            criteria['action'] = 'delete'
        else:
            criteria['action'] = 'modify'
        if self.attr_combo.currentIndex() == self.attr_combo.count() - 1:
            criteria['attrib'] = None
        else:
            criteria['attrib'] = str(self.attr_combo.currentText())
        srch_str = str(self.srch_txt.displayText())
        if not len(srch_str):
            srch_str = None
        if srch_str is None and criteria['attrib'] is not None:
            title = _t('guiMain', 'Error')
            msg = '<p>{0}'.format(
                _t('guiMain', 'Must enter a value for the attribute selected'))
            return QMessageBox.warning(self, title, msg, QMessageBox.Ok)
        criteria['srch_str'] = srch_str

        criteria['srch_method'] = 'normal'
        if self.srch_method.isChecked():
            criteria['srch_method'] = 'regex'
        if self.newtag_combo.currentIndex() == 0:
            criteria['new_tag'] = None
        else:
            criteria['new_tag'] = str(self.newtag_combo.currentText())
        if criteria['action'] == 'modify' and criteria[
                'new_tag'] is None and self.copy_attr.isChecked():
            title = _t('guiMain', 'Error')
            msg = '<p>{0}'.format(
                _t('guiMain', 'What--exactly--would that achieve?'))
            return QMessageBox.question(self, title, msg, QMessageBox.Ok)

        criteria['new_str'] = str(self.newattr_txt.displayText())
        criteria['copy'] = False
        if self.copy_attr.isChecked():
            criteria['copy'] = True
        if not len(criteria['new_str']):
            criteria['new_str'] = ''

        # Disable the 'Process' button, disable the context customization menu
        self.process_button.setDisabled(True)
        PROCESSED = True

        totals = 0
        self.text_panel.clear()
        self.text_panel.insertHtml('<h4>{}...</h4><br>'.format(
            _t('guiMain', 'Starting')))

        # Loop through the files selected in Sigil's Book View
        for (typ, ident) in self.bk.selected_iter():
            # Skip the ones that aren't the "Text" mimetype.
            if self.bk.id_to_mime(ident) != 'application/xhtml+xml':
                continue
            href = self.bk.id_to_href(ident)
            # Param 1 - the contents of the (x)html file.
            criteria['html'] = self.bk.readfile(ident)
            if not isinstance(criteria['html'], str):
                criteria['html'] = str(criteria['html'], 'utf-8')

            # Hand off the "criteria" parameters dictionary to the parsing engine
            parser = MarkupParser(criteria)

            # Retrieve the new markup and the number of occurrences changed
            try:
                html, occurrences = parser.processml()
            except Exception:
                self.text_panel.insertHtml('<p>{} {}! {}.</p>\n'.format(
                    _t('guiMain', 'Error parsing'), href,
                    _t('guiMain', 'File skipped')))
                continue

            # Report whether or not changes were made (and how many)
            totals += occurrences
            if occurrences:
                # write changed markup back to file
                self.bk.writefile(ident, html)
                self.text_panel.insertHtml(
                    '<p>{} {}:&#160;&#160;&#160;{}</p>\n'.format(
                        _t('guiMain', 'Occurrences found/changed in'), href,
                        int(occurrences)))
            else:
                self.text_panel.insertHtml('<p>{} {}</p>\n'.format(
                    _t('guiMain', 'Criteria not found in'), href))

        # report totals
        if totals:
            self.quit_button.setText(_t('guiMain', 'Commit and Exit'))
            self.quit_button.setToolTip('<p>{}'.format(
                _t('guiMain', 'Commit all changes and exit')))
            self.abort_button.setDisabled(False)
            self.text_panel.insertHtml(
                '<br><h4>{}:&#160;&#160;&#160;{}</h4>'.format(
                    _t('guiMain', 'Total occurrences found/changed'),
                    int(totals)))
        else:
            self.text_panel.insertHtml('<br><h4>{}</h4>'.format(
                _t('guiMain', 'No changes made to book')))
        self.text_panel.insertHtml('<br><h4>{}</h4>'.format(
            _t('guiMain', 'Finished')))

    def _quit_clicked(self):
        self.misc_prefs['windowGeometry'] = self.saveGeometry().toHex().data(
        ).decode('ascii')
        if PROCESSED:
            self.gui_prefs['action'] = self.action_combo.currentIndex()
            self.gui_prefs['tag'] = self.tag_combo.currentIndex()
            self.gui_prefs['attrs'] = self.attr_combo.currentIndex()
        self._ok_to_close = True
        self.close()

    def _abort_clicked(self):
        global BAIL_OUT
        BAIL_OUT = True
        self._ok_to_close = True
        self.close()

    def getAbort(self):
        return BAIL_OUT

    def showConfig(self):
        ''' Launch Customization Dialog '''
        dlg = ConfigDialog(self, self.combobox_values)
        if dlg.exec_() == QDialog.Accepted:
            self.refresh_attr_values()
            self.update_gui()

    def check_for_update(self):
        '''Use updatecheck.py to check for newer versions of the plugin'''
        last_time_checked = self.update_prefs['last_time_checked']
        last_online_version = self.update_prefs['last_online_version']
        chk = UpdateChecker(last_time_checked, last_online_version, self.bk._w)
        update_available, online_version, time = chk.update_info()
        # update preferences with latest date/time/version
        self.update_prefs['last_time_checked'] = time
        if online_version is not None:
            self.update_prefs['last_online_version'] = online_version
        if update_available:
            return (True, online_version)
        return (False, online_version)

    def closeEvent(self, event):
        if self._ok_to_close:
            event.accept()  # let the window close
        else:
            self._abort_clicked()