コード例 #1
0
 def mouseDoubleClickEvent(self, e):
     add_relationships_menu = QMenu(self._spine_db_editor)
     title = TitleWidgetAction("Add relationships", self._spine_db_editor)
     add_relationships_menu.addAction(title)
     add_relationships_menu.triggered.connect(self._start_relationship)
     self._populate_add_relationships_menu(add_relationships_menu)
     add_relationships_menu.popup(e.screenPos())
コード例 #2
0
 def onOrderTreeRightClicked(self):
     order_tree = self.manual_create_order.findChild(QTreeWidget, "mtree")
     delete_order = self.manual_create_order.findChild(
         QAction, "delete_order")
     if order_tree.currentColumn() >= 0:
         menu = QMenu(order_tree)
         menu.addAction(delete_order)
         menu.popup(QtGui.QCursor.pos())
コード例 #3
0
    def onTableViewColumnClicked(self, index, table_view):
        # up = self.window.findChild(QAction, "action_up")
        # down = self.window.findChild(QAction, "action_down")
        roll = self.window.findChild(QAction, "action_roll")
        delete = self.window.findChild(QAction, "action_delete_column")

        menu = QMenu(table_view)
        menu.addAction(roll)
        menu.addAction(delete)
        menu.popup(QtGui.QCursor.pos())
        return
 def right_click_menu(self, point):
     column = self.header.logicalIndexAt(point.x())
     # show menu about the column if column 1
     if column == 1:
         menu = QMenu(self)
         menu.addAction('Auto label (experimental)', self.auto_label)
         menu.popup(self.header.mapToGlobal(point))
     elif column == 0:
         menu = QMenu(self)
         menu.addAction('Remove empty rows', self.remove_empty_rows)
         menu.popup(self.header.mapToGlobal(point))
コード例 #5
0
 def onHoldingsContextMenu(self, pos):
     index = self.HoldingsTableView.indexAt(pos)
     contextMenu = QMenu(self.HoldingsTableView)
     actionEstimateTax = QAction(text=g_tr('Ledger',
                                           "Estimate Russian Tax"),
                                 parent=self.HoldingsTableView)
     actionEstimateTax.triggered.connect(
         partial(self.estimateRussianTax,
                 self.HoldingsTableView.viewport().mapToGlobal(pos), index))
     contextMenu.addAction(actionEstimateTax)
     contextMenu.popup(self.HoldingsTableView.viewport().mapToGlobal(pos))
コード例 #6
0
    def mousePressEvent(self, qmouseevent):
        super().mousePressEvent(qmouseevent)
        bt = qmouseevent.button()
        if bt == 2:
            menu = QMenu(self)
            for action in self.popup_items:
                _action = menu.addAction(action[0])
                _action.triggered.connect(action[1])

            point = QPoint(qmouseevent.globalX(), qmouseevent.globalY())
            menu.popup(point)
コード例 #7
0
 def _on_context_menu_requested(
     self,
     position: QPoint,
 ) -> None:
     menu = QMenu(self)
     action_clear = menu.addAction('Clear')
     as_signal_instance(action_clear.triggered).connect(
         self._on_clear_action_triggered,
     )
     menu.popup(
         self.mapToGlobal(position),
     )
コード例 #8
0
 def _show_plus_button_context_menu(self, global_pos):
     toolbox = self._parent.db_mngr.parent()
     if toolbox is None:
         return
     ds_urls = {ds.name: ds.project_item.sql_alchemy_url() for ds in toolbox.project_item_model.items("Data Stores")}
     if not ds_urls:
         return
     menu = QMenu(self)
     for name, url in ds_urls.items():
         action = menu.addAction(name, lambda name=name, url=url: self._parent.add_new_tab({url: name}))
         action.setEnabled(bool(url))
     menu.popup(global_pos)
     menu.aboutToHide.connect(menu.deleteLater)
コード例 #9
0
ファイル: trees.py プロジェクト: luiztauffer/pandasVIS
class QTreeCustomPrimary(QTreeWidget):
    def __init__(self, parent):
        QTreeWidget.__init__(self, parent)
        self.parent = parent
        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.contextMenuEvent)

    def contextMenuEvent(self, event):
        """Tests if click was over a valid item."""
        index = self.indexAt(event)
        if index.isValid():
            item = self.itemAt(event)
            name = item.text(0)   # the text of the node
            self.contextMenu1(name=name, event=event)
        else:
            name = None

    def contextMenu1(self, name, event):
        self.menu = QMenu()
        var_name = self.menu.addAction(name)
        f = QtGui.QFont()
        f.setBold(True)
        var_name.setFont(f)
        self.menu.addSeparator()
        act_summary = self.menu.addAction('Summary')
        act_index = self.menu.addAction('Set as Index')
        act_transform = self.menu.addAction('Transform')
        act_groupby = self.menu.addAction('Group-by')
        act_secondary = self.menu.addAction('Move to Secondary')
        act_delete = self.menu.addAction('Delete')

        self.menu.popup(QtGui.QCursor.pos())
        action = self.menu.exec_(self.mapToGlobal(event))

        if action is not None:
            if action.text() == 'Summary':
                print('')
            if action.text() == 'Set as Index':
                pass # self.parent.df.
            if action.text() == 'Transform':
                print('')
            if action.text() == 'Group-by':
                print('')
            if action.text() == 'Move to Secondary':
                move_to_secondary(parent=self.parent, name=name)
            if action.text() == 'Delete':
                self.parent.df.drop(name, axis=1, inplace=True)
                self.parent.primary_names = self.parent.df.keys().tolist()
                self.parent.console.push_vars({'df': self.parent.df})
                self.parent.init_trees()
コード例 #10
0
ファイル: reference_data.py プロジェクト: kostafey/jal
 def onDataViewContextMenu(self, pos):
     if not self.group_id:
         return
     index = self.DataView.indexAt(pos)
     menu_title = QWidgetAction(self.DataView)
     title_lbl = QLabel()
     title_lbl.setText(g_tr('ReferenceDataDialog', "Change type to:"))
     menu_title.setDefaultWidget(title_lbl)
     contextMenu = QMenu(self.DataView)
     contextMenu.addAction(menu_title)
     contextMenu.addSeparator()
     combo_model = self.GroupCombo.model()
     for i in range(self.GroupCombo.count()):
         type_id = combo_model.data(combo_model.index(i, combo_model.fieldIndex(self.group_fkey_field)))
         contextMenu.addAction(self.GroupCombo.itemText(i), partial(self.updateItemType, index, type_id))
     contextMenu.popup(self.DataView.viewport().mapToGlobal(pos))
コード例 #11
0
 def hoizontalMenu(self, event):
     menu = QMenu(self)
     actionAddColRight = QAction("Add column to the right", self)
     actionAddColLeft = QAction("Add column to the left ", self)
     menu.addAction(actionAddColRight)
     menu.addAction(actionAddColLeft)
     menu.popup(QCursor.pos())
     _col = self.ui.tb_DataFrame.horizontalHeader().logicalIndexAt(event)
     action = menu.exec_()
     # action = menu.exec_()
     if action == actionAddColRight:
         self.OnNewVar(_col + 1)
         return
     if action == actionAddColLeft:
         self.OnNewVar(_col)
         return
コード例 #12
0
class TableView(QTableView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.row = 0
        self.column = 0
        self.clicked.connect(self.check_column)

    def check_column(self, index):
        if index.column() == len(self.model().metadata["columns"]) + 1:
            self.delete_row(index.row())

    def contextMenuEvent(self, event):
        self.row = self.rowAt(event.y())
        self.column = self.columnAt(event.x())

        if self.row == -1 or self.column == -1:
            return
        else:
            self.menu = QMenu(self)
            addRow = QAction('Add new row', self)
            deleteRow = QAction('Remove row', self)

            addRow.triggered.connect(self.add_row)
            deleteRow.triggered.connect(self.delete_row)

            self.menu.addAction(addRow)
            self.menu.addAction(deleteRow)
            self.menu.popup(QtGui.QCursor.pos())

    def delete_row(self, row=None):
        alert = QMessageBox()
        alert.setWindowTitle("Action Dialog")
        alert.setText("Are you sure you want to delete this row?")
        alert.setStandardButtons(QMessageBox.No | QMessageBox.Yes)
        alert.setDefaultButton(QMessageBox.No)
        response = alert.exec_()

        if response == QMessageBox.No:
            return
        else:
            if row == None:
                self.model().removeRows(self.row, 1, QModelIndex())
            else:
                self.model().removeRows(row, 1, QModelIndex())

    def add_row(self):
        self.model().insertRows(self.row, 1, QModelIndex())
コード例 #13
0
ファイル: window.py プロジェクト: alek9z/dataMole
 def createWorkbenchPopupMenu(self, index: QModelIndex) -> None:
     # Create a popup menu when workbench is right-clicked over a valid frame name
     # Menu display delete and remove options
     frameName: str = index.data(Qt.DisplayRole)
     pMenu = QMenu(self)
     # Reuse MainWindow actions
     csvAction = self.parentWidget().aWriteCsv
     pickleAction = self.parentWidget().aWritePickle
     # Set correct args for the clicked row
     csvAction.setOperationArgs(w=self.workbenchModel, frameName=frameName)
     pickleAction.setOperationArgs(w=self.workbenchModel,
                                   frameName=frameName)
     deleteAction = QAction('Remove', pMenu)
     deleteAction.triggered.connect(
         lambda: self.workbenchModel.removeRow(index.row()))
     pMenu.addActions([csvAction, pickleAction, deleteAction])
     pMenu.popup(QtGui.QCursor.pos())
コード例 #14
0
ファイル: main.py プロジェクト: mattdoiron/idfplus
    def custom_table_context_menu(self, position):

        # Create a menu and populate it with actions
        menu = QMenu(self)
        menu.addAction(self.undoAct)
        menu.addAction(self.redoAct)
        menu.addSeparator()
        menu.addAction(self.copyObjAct)
        menu.addAction(self.dupObjAct)
        menu.addAction(self.delObjAct)
        menu.addAction(self.newObjAct)
        menu.addAction(self.cutObjAct)
        menu.addSeparator()
        menu.addMenu(self.jumpToMenu)
        menu.addSeparator()
        menu.addAction(self.findThisAct)
        menu.popup(self.classTable.viewport().mapToGlobal(position))
        self.mouse_position = position
コード例 #15
0
 def onOperationContextMenu(self, pos):
     self.current_index = self.table_view.indexAt(pos)
     contextMenu = QMenu(self.table_view)
     actionReconcile = QAction(text=g_tr('LedgerOperationsView',
                                         "Reconcile"),
                               parent=self)
     actionReconcile.triggered.connect(self.reconcileAtCurrentOperation)
     actionCopy = QAction(text=g_tr('LedgerOperationsView', "Copy"),
                          parent=self)
     actionCopy.triggered.connect(self.copyOperation)
     actionDelete = QAction(text=g_tr('LedgerOperationsView', "Delete"),
                            parent=self)
     actionDelete.triggered.connect(self.deleteOperation)
     contextMenu.addAction(actionReconcile)
     contextMenu.addSeparator()
     contextMenu.addAction(actionCopy)
     contextMenu.addAction(actionDelete)
     contextMenu.popup(self.table_view.viewport().mapToGlobal(pos))
コード例 #16
0
 def onBackTestTreeRightClicked(self):
     menu = QMenu(self.backtest_tree)
     if self.backtest_tree.currentItem().whatsThis(0) == "option":
         #
         menu.addAction(self.add_option_underlying)
     else:
         whats_this = self.backtest_tree.currentItem().whatsThis(0)
         if whats_this == "option_underlying":
             menu.addAction(self.add_option_group)
             # menu.addAction(self.add_option_contract)
             menu.addAction(self.delete_backtest_tree_item)
         elif whats_this == "option_group":
             menu.addAction(self.add_option_contract)
             menu.addAction(self.delete_backtest_tree_item)
         elif whats_this == "option_contract":
             menu.addAction(self.delete_backtest_tree_item)
         else:
             menu.addAction(self.no_support)
     menu.popup(QtGui.QCursor.pos())
コード例 #17
0
class AccountManagerWidget(QtWidgets.QTableView):
    def __init__(self, parent, model):
        super(AccountManagerWidget, self).__init__(parent)
        print(dir(Qt.WindowFlags))
        self.setWindowFlags(Qt.Window)
        self.setModel(model.accounts_model)
        self.menu = QMenu(self)
        self.model = model
        #alot of lines jst for a menu but which is bad
        self.edit_action = QtWidgets.QAction('edit', self)
        self.add_action = QAction('add an account', self)
        self.remove_action = QAction('delete', self)
        self.remove_action.triggered.connect(self.delete_row)
        self.add_action.triggered.connect(self.add_account)
        self.edit_action.triggered.connect(self.edit_account)
        self.menu.addAction(self.edit_action)
        self.menu.addAction(self.add_action)
        self.menu.addAction(self.remove_action)
        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)

    def contextMenuEvent(self, e):
        if not self.model.is_admin(): return
        self.menu.popup(QtGui.QCursor.pos())

    def delete_row(self):
        rows = self.selectionModel().currentIndex().row()
        print("row is", rows)
        self.model.accounts_model.removeRows(rows, 1)

    def add_account(self):
        di = AddAccountDialog(self, self.model)
        res = di.exec_()
        if res: return
        show_message("couldn't add the account")

    def edit_account(self):
        #this is bad stuff
        row = self.selectionModel().currentIndex().row()
        account = self.model.accounts_model.accounts[row]
        di = EditAccountDialog(self, account, self.model, account.username)
        ok = di.exec_()
        if ok: return
コード例 #18
0
 def contextMenuEvent(self, event):
     index = self.tabAt(event.pos())
     if self._plus_button.underMouse():
         self._show_plus_button_context_menu(event.globalPos())
         return
     if self.tabButton(index, QTabBar.RightSide).underMouse():
         return
     db_editor = self._parent.tab_widget.widget(index)
     if db_editor is None:
         return
     menu = QMenu(self)
     others = self._parent.others()
     if others:
         move_tab_menu = menu.addMenu("Move tab to another window")
         move_tab_to_new_window = move_tab_menu.addAction(
             "New window",
             lambda _=False, index=index: self._parent.move_tab(
                 index, None))
         for other in others:
             move_tab_menu.addAction(
                 other.name(),
                 lambda _=False, index=index, other=other: self._parent.
                 move_tab(index, other))
     else:
         move_tab_to_new_window = menu.addAction(
             "Move tab to new window",
             lambda _=False, index=index: self._parent.move_tab(
                 index, None))
     move_tab_to_new_window.setEnabled(self.count() > 1)
     menu.addSeparator()
     menu.addAction(db_editor.url_toolbar.reload_action)
     db_url_codenames = db_editor.db_url_codenames
     menu.addAction(
         QIcon(CharIconEngine("\uf24d")),
         "Duplicate",
         lambda _=False, index=index + 1, db_url_codenames=db_url_codenames:
         self._parent.insert_new_tab(index, db_url_codenames),
     )
     menu.popup(event.globalPos())
     menu.aboutToHide.connect(menu.deleteLater)
     event.accept()
コード例 #19
0
ファイル: modlist.py プロジェクト: JARVIS-AI/w3modmanager
    def showContextMenu(self, pos: QPoint) -> None:
        mods = self.getSelectedMods()
        if not mods:
            return
        menu = QMenu(self)
        actionOpen = menu.addAction(QIcon(str(getRuntimePath('resources/icons/open-folder.ico'))), '&Open Directory')
        actionOpen.triggered.connect(lambda: [
            util.openDirectory(self.modmodel.getModPath(mod))  # type: ignore
            for mod in mods
        ])
        menu.addSeparator()
        actionEnable = menu.addAction('&Enable')
        actionEnable.triggered.connect(lambda: [
            asyncio.create_task(self.enableSelectedMods(True))
        ])
        actionEnable.setEnabled(not all(mod.enabled for mod in mods))
        actionDisable = menu.addAction('&Disable')
        actionDisable.triggered.connect(lambda: [
            asyncio.create_task(self.enableSelectedMods(False))
        ])
        actionDisable.setEnabled(not all(not mod.enabled for mod in mods))
        menu.addSeparator()
        actionUninstall = menu.addAction('&Uninstall')
        actionUninstall.triggered.connect(lambda: [
            asyncio.create_task(self.deleteSelectedMods())
        ])
        menu.addSeparator()
        actionOpenNexus = menu.addAction(
            QIcon(str(getRuntimePath('resources/icons/browse.ico'))), 'Open &Nexus Mods page')
        actionOpenNexus.triggered.connect(lambda: [
            QDesktopServices.openUrl(QUrl(f'https://www.nexusmods.com/witcher3/mods/{modid}'))
            for modid in {mod.modid for mod in mods if mod.modid > 0}
        ])
        actionOpenNexus.setEnabled(not all(mod.modid <= 0 for mod in mods))

        menu.popup(self.viewport().mapToGlobal(pos))
コード例 #20
0
class MyTableView(QTableView):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.contextMenu = QMenu()
        self.contextMenu.addAction("copy").triggered.connect(
            self.copySelectionToClipboard)
        headerView: QHeaderView = self.horizontalHeader()
        headerView.setSectionResizeMode(QHeaderView.ResizeToContents)

    def copySelectionToClipboard(self):
        indexes = self.selectedIndexes()
        indexes.sort(key=lambda i: (i.row(), i.column()))
        model = self.model()
        stringIO = io.StringIO()
        last = indexes[0].row()
        for index in indexes:
            if index.row() != last:
                last = index.row()
                stringIO.write('\n')
            info = model.data(index, Qt.DisplayRole)
            if info is None:
                info = ''
            stringIO.write(info)
            stringIO.write('\t')
        clipboard = QApplication.clipboard()
        clipboard.setText(stringIO.getvalue())

    def keyPressEvent(self, event):
        if event.matches(QKeySequence.Copy):
            self.copySelectionToClipboard()
            event.accept()
        else:
            super().keyPressEvent(event)

    def contextMenuEvent(self, event: QContextMenuEvent):
        self.contextMenu.popup(event.globalPos())
コード例 #21
0
class MainWindow(Ui_Form):
    def __init__(self, parent=None, args=None):
        super(MainWindow, self).__init__(parent)
        self.appname = "poliBeePsync"
        self.settings_fname = 'pbs-settings.ini'
        self.data_fname = 'pbs.data'
        self.setupUi(self)
        self.w = QWidget()

        self.status_signal = MySignal()
        self.status_signal.sig.connect(self.update_status_bar)

        self.logging_signal = MySignal()
        self.logging_signal.sig.connect(self.myStream_message)
        logging_console_hdl = SignalLoggingHandler(self.logging_signal)
        logger.addHandler(logging_console_hdl)
        commonlogger.addHandler(logging_console_hdl)

        self.timer = QTimer(self)

        # settings_path is a string containing the path to settings
        self.settings_path = None
        # settings is a dictionary of settings
        self.settings = None
        # load_settings() sets settings_path and settings
        self.load_settings()
        self.load_data()
        if args.default_timeout:
            self.user.default_timeout = args.default_timeout

        self.timer.timeout.connect(self.syncfiles)
        self.timer.start(1000 * 60 * int(self.settings['UpdateEvery']))

        self.loginthread = LoginThread(self.user, self)

        self.loginthread.signal_error.sig.connect(self.update_status_bar)
        self.loginthread.signal_ok.sig.connect(self.update_status_bar)

        self.refreshcoursesthread = RefreshCoursesThread(self.user, self)
        self.refreshcoursesthread.dumpuser.sig.connect(self.dumpUser)
        self.refreshcoursesthread.newcourses.sig.connect(self.addtocoursesview)
        self.refreshcoursesthread.newcourses.sig.connect(self.syncnewcourses)
        self.refreshcoursesthread.removable.sig.connect(self.rmfromcoursesview)

        self.downloadthread = DownloadThread(self.user,
                                             self.settings['RootFolder'], self)
        self.downloadthread.dumpuser.sig.connect(self.dumpUser)
        self.downloadthread.download_signal.connect(
            self.update_course_download)
        self.downloadthread.initial_sizes.connect(self.setinizialsizes)
        self.downloadthread.date_signal.connect(self.update_file_localtime)

        self._window.userCode.setText(str(self.user.username))
        self._window.userCode.editingFinished.connect(self.setusercode)
        self._window.password.setText(self.user.password)
        self._window.password.editingFinished.connect(self.setpassword)
        self._window.trylogin.clicked.connect(self.testlogin)

        self._window.courses_model = CoursesListModel(
            self.user.available_courses)
        self._window.coursesView.setModel(self._window.courses_model)
        self._resizeview()
        self._window.refreshCourses.clicked.connect(self.refreshcourses)

        self._window.syncNow.clicked.connect(self.syncfiles)

        self._window.rootfolder.setText(self.settings['RootFolder'])
        self._window.rootfolder.textChanged.connect(self.rootfolderslot)

        init_checkbox(self._window.addSyncNewCourses,
                      self.settings,
                      'SyncNewCourses',
                      state_slot=self.syncnewslot)

        init_checkbox(self._window.startupSync,
                      self.settings,
                      'SyncOnStartup',
                      state_slot=self.sync_on_startup_slot)

        self._window.timerMinutes.setValue(int(self.settings['UpdateEvery']))
        self._window.timerMinutes.valueChanged.connect(self.updateminuteslot)

        self._window.changeRootFolder.clicked.connect(self.chooserootdir)
        self._window.version_label.setText(
            "Current version: {}".format(__version__))
        self._window.check_version.clicked.connect(self.checknewversion)

        self._window.about.clicked.connect(self.showabout)

        self.trayIconMenu = QMenu()
        self.trayIcon = QSystemTrayIcon(self.icon, self.w)
        self.trayIcon.activated.connect(self._activate_traymenu)
        self.createTray()

        try:
            if args.sync_on_startup or \
               self.settings['SyncOnStartup'] == str(True):
                self.syncfiles()
        except KeyError:
            pass
        if args.sync_interval:
            logger.info('Sync interval overridden with '
                        f'{args.sync_interval} minutes')
            self.timer.start(1000 * 60 * args.sync_interval)

    @Slot()
    def showabout(self, **kwargs):
        msgBox = QMessageBox(self._window)
        msgBox.setTextFormat(Qt.RichText)
        msgBox.setWindowTitle('About poliBeePSync')
        text = """
<html>
<head/>
<body>
  <p>
    poliBeePsync is a program written by Davide Olianas and Raffaele Di Campli
    released under GNU GPLv3+.
    More information is available on the
    <a href=\"https://github.com/Jacotsu/polibeepsync\">
    <span style=\" text-decoration: underline; color:#0000ff;\">
    official github</span></a>.
    Feel free to contact us at
    <a href=\"mailto:[email protected]\">[email protected]</a> for
    suggestions and bug reports.
  </p>
  <p>
  Want to learn how to make softwares like this? Then join <br>
    <a href='https://poul.org/'>
      <img src=':/root/imgs/PinguiniStilNovoFullLogoBlack.svg'>
    </a>
  </p>
  <p>
    <a href='https://liberapay.com/jacotsu/donate'>
        Want to offer me a sandwich?
    </a>
  </p>
</body>
</html>
"""
        msgBox.setInformativeText(text)
        msgBox.exec()

    @Slot()
    def _resizeview(self, **kwargs):
        self._window.coursesView.setColumnWidth(3, 160)
        self._window.coursesView.resizeColumnToContents(1)
        self._window.coursesView.setColumnWidth(0, 320)

    def checknewversion(self):
        rawdata = requests.get('https://pypi.python.org/pypi/'
                               'poliBeePsync/json')
        latest = json.loads(rawdata.text)['info']['version']
        if latest != __version__:
            newtext = 'Current version: {}. Latest version: {}. '\
                'Click <a href="https://jacotsu.github.io/polibeepsync/build/'\
                'html/installation.html">here</a>'\
                ' to find out how to upgrade'.format(__version__, latest)
        else:
            newtext = "Current version: {} up-to-date.".format(__version__)
        self._window.version_label.setText(newtext)

    def _update_time(self, folder, file, path_list):
        logger.debug(f'inside {folder.name}')
        for path in path_list:
            logger.debug(f'namegoto: {path}')
            folder_dict = {'name': path}
            fakefolder = Folder(folder_dict)
            logger.debug(f'contained folders:  {folder.folders}')
            ind = folder.folders.index(fakefolder)
            goto = folder.folders[ind]
            self._update_time(goto, file, path_list)

        if file in folder.files:
            ind = folder.files.index(file)
            thisfile = folder.files[ind]
            thisfile.local_creation_time = file.local_creation_time

    @Slot(tuple)
    def update_file_localtime(self, data, **kwargs):
        course, coursefile, path = data
        rootpath = os.path.join(self.settings['RootFolder'],
                                course.save_folder_name)
        if path.startswith(rootpath):
            partial = path[len(rootpath):]
        path_list = filter(None, partial.split(os.path.sep))
        self._update_time(course.documents, coursefile, path_list)

    @Slot(Course)
    def update_course_download(self, course, **kwargs):
        if course in self.user.available_courses:
            updating = self.user.available_courses[course.name]
            updating.downloaded_size = course.downloaded_size
            row = self._window.courses_model.courses.index(updating)
            where = self._window.courses_model.index(row, 3)
            self._window.courses_model.dataChanged.emit(where, where)

    @Slot(Course)
    def setinizialsizes(self, course, **kwargs):
        if course in self.user.available_courses:
            updating = self.user.available_courses[course.name]
            updating.downloaded_size = course.downloaded_size
            updating.size = course.size
            row = self._window.courses_model.courses.index(updating)
            where = self._window.courses_model.index(row, 3)
            self._window.courses_model.dataChanged.emit(where, where)
            self.dumpUser()

    @Slot(list)
    def syncnewcourses(self, newlist):
        if self.settings['SyncNewCourses'] == 'True':
            for elem in newlist:
                elem.sync = True

    def load_settings(self):
        for path in [
                user_config_dir(self.appname),
                user_data_dir(self.appname)
        ]:
            try:
                os.makedirs(path, exist_ok=True)
            except OSError:
                logger.critical('OSError while calling os.makedirs.',
                                exc_info=True)
                logger.critical(f"I couldn't create {path}.\nStart"
                                " poliBeePsync with --log-level=debug "
                                "error to get more details.")
        self.settings_path = os.path.join(user_config_dir(self.appname),
                                          self.settings_fname)
        defaults = {
            # Update every 8 hours
            'UpdateEvery': '480',
            'RootFolder': os.path.join(os.path.expanduser('~'), self.appname),
            'SyncNewCourses': 'False',
            'SyncOnStartup': 'False'
        }
        self.settings = filesettings.settingsFromFile(self.settings_path,
                                                      defaults)

    def load_data(self):
        try:
            with open(
                    os.path.join(user_data_dir(self.appname), self.data_fname),
                    'rb') as f:
                self.user = pickle.load(f)
                self.user.password = keyring\
                    .get_password('beep.metid.polimi.it',
                                  self.user.username)
                logger.info("Data has been loaded successfully.")
        except (EOFError, pickle.PickleError):
            logger.error('Settings corrupted', exc_info=True)
            self.user = User('', '')

        except FileNotFoundError:
            logger.error('Settings file not found.')
            self.user = User('', '')
            logger.error("I couldn't find data in the"
                         " predefined directory. Ignore this"
                         "message if you're using poliBeePsync"
                         " for the first time.")

    @Slot(str)
    def update_status_bar(self, status):
        self._window.statusbar.showMessage(status)

    @Slot(int)
    def syncnewslot(self, state):
        if state == 2:
            self.settings['SyncNewCourses'] = 'True'
            logger.info('New courses will now be automatically synced')
        else:
            self.settings['SyncNewCourses'] = 'False'
            logger.info('New courses will NOT be automatically synced')
        filesettings.settingsToFile(self.settings, self.settings_path)

    @Slot(int)
    def sync_on_startup_slot(self, state):
        if state == 2:
            self.settings['SyncOnStartup'] = 'True'
            logger.info('All courses will be synced at startup')
        else:
            self.settings['SyncOnStartup'] = 'False'
            logger.info('No course will be synced at startup')
        filesettings.settingsToFile(self.settings, self.settings_path)

    @Slot(int)
    def updateminuteslot(self, minutes):
        self.settings['UpdateEvery'] = str(minutes)
        filesettings.settingsToFile(self.settings, self.settings_path)
        self.timer.start(1000 * 60 * int(self.settings['UpdateEvery']))
        logger.info('All courses will be automatically synced every '
                    f'{self.settings["UpdateEvery"]} minutes')

    @Slot(str)
    def rootfolderslot(self, path):
        self.settings['RootFolder'] = path
        filesettings.settingsToFile(self.settings, self.settings_path)
        logger.info(f'Root folder set to: {path}')

    @Slot()
    def chooserootdir(self):
        currentdir = self.settings['RootFolder']
        flags = QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly
        newroot = QFileDialog.getExistingDirectory(None, "Open Directory",
                                                   currentdir, flags)
        if newroot != "" and str(newroot) != currentdir:
            self.settings['RootFolder'] = str(newroot)
            filesettings.settingsToFile(self.settings, self.settings_path)
            self._window.rootfolder.setText(newroot)
            # we delete the already present downloadthread and recreate it
            # because otherwise it uses the old download folder. I don't know
            # if there's a cleaner approach
            del self.downloadthread
            self.downloadthread = DownloadThread(self.user,
                                                 self.settings['RootFolder'],
                                                 self)
            self.downloadthread.dumpuser.sig.connect(self.dumpUser)
            self.dumpUser()

    @Slot()
    def setusercode(self):
        newcode = self._window.userCode.text()
        try:
            if len(newcode) == 8:
                self.user.username = newcode
                logger.info(f'User code changed to {newcode}.')
                keyring.set_password('beep.metid.polimi.it',
                                     self.user.username, self.user.password)
        except OSError:
            logger.critical("I couldn't save data to disk. Run"
                            " poliBeePsync with option --log-level=debug"
                            " error to get more details.")
            logger.error(
                'OSError raised while trying to write the User'
                'instance to disk.',
                exc_info=True)

    @Slot()
    def setpassword(self):
        newpass = self._window.password.text()
        self.user.password = newpass
        try:
            keyring.set_password('beep.metid.polimi.it', self.user.username,
                                 self.user.password)
            logger.info("Password changed.")
        except OSError:
            logger.critical("I couldn't save data to disk. Run"
                            " poliBeePsync with option --log-level=debug"
                            " error to get more details.")
            logger.error(
                'OSError raised while trying to write the User'
                'instance to disk.',
                exc_info=True)

    @Slot()
    def testlogin(self):
        if not self.loginthread.isRunning():
            self.loginthread.exiting = False
            self.loginthread.start()
            self.status_signal.sig.emit("Logging in, please wait.")

    @Slot(list)
    def addtocoursesview(self, addlist):
        for elem in addlist:
            self._window.courses_model.insertRows(0, 1, elem)

    @Slot(list)
    def rmfromcoursesview(self, removelist):
        for elem in removelist:
            index = self._window.courses_model.courses.index(elem)
            self._window.courses_model.removeRows(index, 1)

    @Slot()
    def dumpUser(self):
        # we don't use the message...
        with open(os.path.join(user_data_dir(self.appname), self.data_fname),
                  'wb') as f:
            tmp_pw = self.user.password
            self.user.password = ''
            pickle.dump(self.user, f)
            self.user.password = tmp_pw

    @Slot()
    def refreshcourses(self):
        self.status_signal.sig.emit('Searching for online updates...'
                                    'this may take a while.')
        if not self.loginthread.isRunning():
            self.loginthread.exiting = False
            self.loginthread.signal_ok.sig.connect(self.do_refreshcourses)
            self.loginthread.start()

    def do_refreshcourses(self):
        self.loginthread.signal_ok.sig.disconnect(self.do_refreshcourses)
        if not self.refreshcoursesthread.isRunning():
            self.refreshcoursesthread.start()

    @Slot()
    def syncfiles(self):
        # we delete the already present downloadthread and recreate it
        # because otherwise it uses the old download folder. I don't know
        # if there's a cleaner approach
        del self.downloadthread
        self.downloadthread = DownloadThread(self.user,
                                             self.settings['RootFolder'], self)
        self.downloadthread.dumpuser.sig.connect(self.dumpUser)

        self.refreshcoursesthread.finished.connect(self.do_syncfiles)
        self.refreshcourses()

    @Slot()
    def do_syncfiles(self):
        self.refreshcoursesthread.finished.disconnect(self.do_syncfiles)
        self.status_signal.sig.emit('Started syncing.')
        self.downloadthread.start()

    @Slot(str)
    def myStream_message(self, message):
        self._window.status.moveCursor(QTextCursor.End)
        self._window.status.insertPlainText(message + "\n")

    def restore_window(self):
        self._window.setWindowState(self.windowState() & ~Qt.WindowMinimized
                                    | Qt.WindowActive)
        self._window.show()

    def createTray(self):
        restoreAction = QAction("&Restore",
                                self,
                                triggered=self.restore_window)
        quitAction = QAction("&Quit",
                             self,
                             triggered=QApplication.instance().quit)
        self.trayIconMenu.addAction(restoreAction)
        self.trayIconMenu.addAction(quitAction)
        self.trayIcon.setContextMenu(self.trayIconMenu)
        self.trayIcon.show()

    @Slot(str)
    def _activate_traymenu(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
            self.restore_window()
        else:
            self.trayIconMenu.activateWindow()
            self.trayIconMenu.popup(QCursor.pos())

    def closeEvent(self, event):
        self._window.hide()
        event.ignore()
コード例 #22
0
ファイル: Qclass.py プロジェクト: ikarosf/newKFguiFAKE
class STATCardPanel(QWidget):
    def __init__(self, parent=None, cardList=None, cardlistcomboBox=None):
        super(STATCardPanel, self).__init__(parent)
        self.cardList = cardList
        self.cardlistcomboBox = cardlistcomboBox
        self.gridLayout = QGridLayout(self)

        self.nicknamelabel = QLabel(self)
        self.nicknamelabel.setText("卡片别名")

        self.nicknamelineedit = QLineEdit(self)

        self.cardtypelabel = QLabel(self)
        self.cardtypelabel.setText("卡片类型")

        self.cardtypecomboBox = cardCharacterComboBox(self)

        self.wishpanel = wishPanel()
        self.amuletpanel = amuletPanel()

        # gridLayout1__________________________

        self.gridLayout1 = QGridLayout()

        self.ADlabel = QLabel(self)
        self.ADlabel.setText("物攻")
        self.gridLayout1.addWidget(self.ADlabel, 0, 0, 1, 1)

        self.ADLineEdit = intLineEdit(self)
        self.gridLayout1.addWidget(self.ADLineEdit, 0, 1, 1, 1)

        self.APlabel = QLabel(self)
        self.APlabel.setText("魔攻")
        self.gridLayout1.addWidget(self.APlabel, 1, 0, 1, 1)

        self.APLineEdit = intLineEdit(self)
        self.gridLayout1.addWidget(self.APLineEdit, 1, 1, 1, 1)

        self.ADClabel = QLabel(self)
        self.ADClabel.setText("物穿")
        self.gridLayout1.addWidget(self.ADClabel, 2, 0, 1, 1)

        self.ADCLineEdit = intLineEdit(self)
        self.gridLayout1.addWidget(self.ADCLineEdit, 2, 1, 1, 1)

        self.ADCALineEdit = intLineEdit(self)
        self.gridLayout1.addWidget(self.ADCALineEdit, 2, 2, 1, 1)

        self.APClabel = QLabel(self)
        self.APClabel.setText("魔穿")
        self.gridLayout1.addWidget(self.APClabel, 3, 0, 1, 1)

        self.APCLineEdit = intLineEdit(self)
        self.gridLayout1.addWidget(self.APCLineEdit, 3, 1, 1, 1)

        self.APCALineEdit = intLineEdit(self)
        self.gridLayout1.addWidget(self.APCALineEdit, 3, 2, 1, 1)

        # gridLayout2__________________________

        self.gridLayout2 = QGridLayout()

        self.RTKlabel = QLabel(self)
        self.RTKlabel.setText("绝对攻击")
        self.gridLayout2.addWidget(self.RTKlabel, 0, 0, 1, 1)

        self.RTKLineEdit = intLineEdit(self)
        self.gridLayout2.addWidget(self.RTKLineEdit, 0, 1, 1, 1)

        self.CRITClabel = QLabel(self)
        self.CRITClabel.setText("暴击穿透")
        self.gridLayout2.addWidget(self.CRITClabel, 1, 0, 1, 1)

        self.CRITCLineEdit = intLineEdit(self)
        self.gridLayout2.addWidget(self.CRITCLineEdit, 1, 1, 1, 1)

        self.SPEEDlabel = QLabel(self)
        self.SPEEDlabel.setText("速度")
        self.gridLayout2.addWidget(self.SPEEDlabel, 2, 0, 1, 1)

        self.SPEEDLineEdit = intLineEdit(self)
        self.gridLayout2.addWidget(self.SPEEDLineEdit, 2, 1, 1, 1)

        self.WDEFlabel = QLabel(self)
        self.WDEFlabel.setText("物防")
        self.gridLayout2.addWidget(self.WDEFlabel, 3, 0, 1, 1)

        self.WDEFLineEdit = intLineEdit(self)
        self.gridLayout2.addWidget(self.WDEFLineEdit, 3, 1, 1, 1)

        self.WDEFALineEdit = intLineEdit(self)
        self.gridLayout2.addWidget(self.WDEFALineEdit, 3, 2, 1, 1)

        self.MDEFlabel = QLabel(self)
        self.MDEFlabel.setText("魔防")
        self.gridLayout2.addWidget(self.MDEFlabel, 4, 0, 1, 1)

        self.MDEFLineEdit = intLineEdit(self)
        self.gridLayout2.addWidget(self.MDEFLineEdit, 4, 1, 1, 1)

        self.MDEFALineEdit = intLineEdit(self)
        self.gridLayout2.addWidget(self.MDEFALineEdit, 4, 2, 1, 1)

        # gridLayout3__________________________

        self.gridLayout3 = QGridLayout()

        self.HEALTHlabel = QLabel(self)
        self.HEALTHlabel.setText("生命")
        self.gridLayout3.addWidget(self.HEALTHlabel, 0, 0, 1, 1)

        self.HEALTHLineEdit = intLineEdit(self)
        self.gridLayout3.addWidget(self.HEALTHLineEdit, 0, 1, 1, 1)

        self.RECOVERlabel = QLabel(self)
        self.RECOVERlabel.setText("回血")
        self.gridLayout3.addWidget(self.RECOVERlabel, 1, 0, 1, 1)

        self.RECOVERLineEdit = intLineEdit(self)
        self.gridLayout3.addWidget(self.RECOVERLineEdit, 1, 1, 1, 1)

        self.RECOVERALineEdit = intLineEdit(self)
        self.gridLayout3.addWidget(self.RECOVERALineEdit, 1, 2, 1, 1)

        self.SHIELDlabel = QLabel(self)
        self.SHIELDlabel.setText("护盾")
        self.gridLayout3.addWidget(self.SHIELDlabel, 2, 0, 1, 1)

        self.SHIELDLineEdit = intLineEdit(self)
        self.gridLayout3.addWidget(self.SHIELDLineEdit, 2, 1, 1, 1)

        self.SHIELDRECOVERlabel = QLabel(self)
        self.SHIELDRECOVERlabel.setText("回盾")
        self.gridLayout3.addWidget(self.SHIELDRECOVERlabel, 3, 0, 1, 1)

        self.SHIELDRECOVERLineEdit = intLineEdit(self)
        self.gridLayout3.addWidget(self.SHIELDRECOVERLineEdit, 3, 1, 1, 1)

        self.SHIELDRECOVERALineEdit = intLineEdit(self)
        self.gridLayout3.addWidget(self.SHIELDRECOVERALineEdit, 3, 2, 1, 1)

        # gridLayout4__________________________

        self.gridLayout4 = QGridLayout()

        self.CRITlabel = QLabel(self)
        self.CRITlabel.setText("暴击")
        self.gridLayout4.addWidget(self.CRITlabel, 0, 0, 1, 1)

        self.CRITLineEdit = intLineEdit(self)
        self.gridLayout4.addWidget(self.CRITLineEdit, 0, 1, 1, 1)

        self.SKILLlabel = QLabel(self)
        self.SKILLlabel.setText("技能")
        self.gridLayout4.addWidget(self.SKILLlabel, 1, 0, 1, 1)

        self.SKILLLineEdit = intLineEdit(self)
        self.gridLayout4.addWidget(self.SKILLLineEdit, 1, 1, 1, 1)

        self.REFLECTlabel = QLabel(self)
        self.REFLECTlabel.setText("反弹")
        self.gridLayout4.addWidget(self.REFLECTlabel, 2, 0, 1, 1)

        self.REFLECTLineEdit = intLineEdit(self)
        self.gridLayout4.addWidget(self.REFLECTLineEdit, 2, 1, 1, 1)

        self.VAMPIRElabel = QLabel(self)
        self.VAMPIRElabel.setText("吸血")
        self.gridLayout4.addWidget(self.VAMPIRElabel, 3, 0, 1, 1)

        self.VAMPIRELineEdit = intLineEdit(self)
        self.gridLayout4.addWidget(self.VAMPIRELineEdit, 3, 1, 1, 1)

        # gridLayout5__________________________

        self.gridLayout5 = QGridLayout()

        self.comboBoxList = []
        for i in range(5, len(all_skill["name"])):
            name = all_skill["name"][i]
            attr = all_skill["data"][i]
            if attr == "XUE":
                continue
            thisCombobox = STATSkillCheckBox(name, attr, self)
            self.comboBoxList.append(thisCombobox)

        thisCombobox = STATSkillCheckBox("荣誉之刃", "BLADE", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("刺杀弓", "ASSBOW", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("手环", "BRACELET", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("秃鹫手套", "VULTURE", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("发饰", "TIARA", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("幽梦匕首", "DAGGER", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("光辉法杖", "WAND", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("荆棘剑盾", "SHIELD", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("天使缎带", "RIBBON", self)
        self.comboBoxList.append(thisCombobox)
        thisCombobox = STATSkillCheckBox("陨铁重剑", "CLAYMORE", self)
        self.comboBoxList.append(thisCombobox)

        try:
            for i in range(5):
                for j in range(5):
                    self.gridLayout5.addWidget(self.comboBoxList[i * 5 + j], i, j, 1, 1)
        except:
            pass

        # gridLayout6__________________________

        self.gridLayout6 = QGridLayout()

        self.savepushButton = QPushButton()
        self.savepushButton.clicked.connect(self.saveMyCard)
        self.savepushButton.setText(u"保存卡片")
        self.gridLayout6.addWidget(self.savepushButton, 0, 0, 1, 1)

        self.editpushButton = QPushButton()
        self.editpushButton.clicked.connect(self.editMyCard)
        self.editpushButton.setText(u"修改卡片")
        self.gridLayout6.addWidget(self.editpushButton, 0, 1, 1, 1)

        self.deletepushButton = QPushButton()
        self.deletepushButton.clicked.connect(self.delMyCard)
        self.deletepushButton.setText(u"删除卡片")
        self.gridLayout6.addWidget(self.deletepushButton, 0, 2, 1, 1)

        # final

        self.rightMenuCreat()
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.rightMenuShow)

        self.gridLayout.addWidget(self.nicknamelabel, 0, 0, 1, 1)  # 卡片别名
        self.gridLayout.addWidget(self.nicknamelineedit, 0, 1, 1, 1)  # 卡片别名QLineEdit
        self.gridLayout.addWidget(self.cardtypelabel, 1, 0, 1, 1)  # 卡片类型
        self.gridLayout.addWidget(self.cardtypecomboBox, 1, 1, 1, 1)  # 卡片类型comboBox
        self.gridLayout.addWidget(self.wishpanel, 2, 0, 1, 1)
        self.gridLayout.addWidget(self.amuletpanel, 2, 1, 1, 1)
        self.gridLayout.addLayout(self.gridLayout1, 3, 0, 2, 1)  # 物攻
        self.gridLayout.addLayout(self.gridLayout2, 3, 1, 2, 1)  # 绝对攻击
        self.gridLayout.addLayout(self.gridLayout3, 5, 0, 2, 1)  # 生命
        self.gridLayout.addLayout(self.gridLayout4, 5, 1, 2, 1)  # 暴击
        self.gridLayout.addLayout(self.gridLayout5, 7, 0, 2, 2)  # 神秘
        self.gridLayout.addLayout(self.gridLayout6, 9, 0, 1, 2)  # 按钮

    def makeMyCard(self):
        nickname = self.nicknamelineedit.text()
        cardType = self.cardtypecomboBox.currentIndex()

        wishSet = WishSet(self.wishpanel.getWishLevelList())
        amuletclass = amuletClass(self.amuletpanel.getAmuletLevelList())

        attrs1 = []
        attrs1.append(self.ADLineEdit.getValue())
        attrs1.append(self.APLineEdit.getValue())
        attrs1.append(self.RTKLineEdit.getValue())
        attrs1.append(self.ADCLineEdit.getValue())
        attrs1.append(self.ADCALineEdit.getValue())
        attrs1.append(self.APCLineEdit.getValue())
        attrs1.append(self.APCALineEdit.getValue())
        attrs1.append(self.CRITCLineEdit.getValue())

        attrs2 = []
        attrs2.append(self.SPEEDLineEdit.getValue())
        attrs2.append(self.WDEFLineEdit.getValue())
        attrs2.append(self.WDEFALineEdit.getValue())
        attrs2.append(self.MDEFLineEdit.getValue())
        attrs2.append(self.MDEFALineEdit.getValue())

        attrs3 = []
        attrs3.append(self.HEALTHLineEdit.getValue())
        attrs3.append(self.RECOVERLineEdit.getValue())
        attrs3.append(self.RECOVERALineEdit.getValue())
        attrs3.append(self.SHIELDLineEdit.getValue())
        attrs3.append(self.SHIELDRECOVERLineEdit.getValue())
        attrs3.append(self.SHIELDRECOVERALineEdit.getValue())

        attrs4 = []
        attrs4.append(self.CRITLineEdit.getValue())
        attrs4.append(self.SKILLLineEdit.getValue())
        attrs4.append(self.REFLECTLineEdit.getValue())
        attrs4.append(self.VAMPIRELineEdit.getValue())

        attrs5 = [0]
        for i in self.comboBoxList:
            if i.isChecked():
                attrs5[0] += 1
                attrs5.append(i.currentValue())

        return STATCard(cardType, attrs1, attrs2, attrs3, attrs4, attrs5, nickname, wishSet, amuletclass)

    def ableCheck(self):
        return True, ""

    def saveMyCard(self):
        (result, message) = self.ableCheck()
        if not result:
            QMessageBox.critical(self, "错误", message, QMessageBox.Yes)
            return
        try:
            newCard = self.makeMyCard()
            text, ok = QInputDialog.getText(self, '设置卡片显示名', '输入名称:', text=newCard.tostring())
            if ok and text:
                if text in self.cardList.keys():
                    QMessageBox.critical(self, "错误", "保存失败,与已有配置重名", QMessageBox.Yes)
                    return
                self.cardList[text] = newCard
                self.cardlistcomboBox.setCurrentText(text)
        except Exception as err:
            print(err)
            QMessageBox.critical(self, "错误", err.__str__(), QMessageBox.Yes)

    def editMyCard(self):
        (result, message) = self.ableCheck()
        if not result:
            QMessageBox.critical(self, "错误", message, QMessageBox.Yes)
            return
        newCard = self.makeMyCard()
        # index = self.comboBox.currentIndex()
        text = self.cardlistcomboBox.currentText()
        if text == "":
            QMessageBox.critical(self, "错误", "修改失败,空名称", QMessageBox.Yes)
            return
        if text == "新卡片":
            QMessageBox.critical(self, "错误", "模板不可修改", QMessageBox.Yes)
            return
        yes = QMessageBox.question(self, "提问对话框", "确认修改?", QMessageBox.Yes | QMessageBox.No)
        if yes == QMessageBox.Yes:
            self.cardList[text] = newCard

    def delMyCard(self):
        text = self.cardlistcomboBox.currentText()
        if text == "新卡片":
            QMessageBox.critical(self, "错误", "模板不可删除", QMessageBox.Yes)
            return
        yes = QMessageBox.question(self, "提问对话框", "确认删除?", QMessageBox.Yes | QMessageBox.No)
        if yes == QMessageBox.Yes:
            del (self.cardList[text])

    def newMyCard(self):
        self.nicknamelineedit.setText("")
        self.cardtypecomboBox.setCurrentIndex(0)

        wishLevelList = [0, 0, 0, 0, 0, 0, 0]
        self.wishpanel.wishLevelList = wishLevelList
        self.amuletpanel.resetList()

        self.ADLineEdit.setText("")
        self.APLineEdit.setText("")
        self.RTKLineEdit.setText("")
        self.ADCLineEdit.setText("")
        self.ADCALineEdit.setText("")
        self.APCLineEdit.setText("")
        self.APCALineEdit.setText("")
        self.CRITCLineEdit.setText("")

        self.SPEEDLineEdit.setText("")
        self.WDEFLineEdit.setText("")
        self.WDEFALineEdit.setText("")
        self.MDEFLineEdit.setText("")
        self.MDEFALineEdit.setText("")

        self.HEALTHLineEdit.setText("")
        self.RECOVERLineEdit.setText("")
        self.RECOVERALineEdit.setText("")
        self.SHIELDLineEdit.setText("")
        self.SHIELDRECOVERLineEdit.setText("")
        self.SHIELDRECOVERALineEdit.setText("")

        self.CRITLineEdit.setText("")
        self.SKILLLineEdit.setText("")
        self.REFLECTLineEdit.setText("")
        self.VAMPIRELineEdit.setText("")

        for i in self.comboBoxList:
            i.setChecked(False)

    def setMyCard(self, card):
        cardtype = card.cardType
        attrs1 = card.attrs1
        attrs2 = card.attrs2
        attrs3 = card.attrs3
        attrs4 = card.attrs4
        attrs5 = card.attrs5
        if hasattr(card, "nickname"):
            nickname = card.nickname
            self.nicknamelineedit.setText(nickname)
        else:
            self.nicknamelineedit.setText("")

        self.cardtypecomboBox.setCurrentIndex(cardtype)

        if hasattr(card, "wishSet"):
            wishSet = card.wishSet
        else:
            wishSet = WishSet([0, 0, 0, 0, 0, 0, 0])

        if hasattr(card, "amuletclass"):
            amuletclass = card.amuletclass
        else:
            amuletclass = amuletClass([])

        self.wishpanel.wishLevelList = wishSet.getWishLevelList()
        self.amuletpanel.amuletLevelList = amuletclass.getAmuletLevelList()

        self.ADLineEdit.setText(attrs1[0])
        self.APLineEdit.setText(attrs1[1])
        self.RTKLineEdit.setText(attrs1[2])
        self.ADCLineEdit.setText(attrs1[3])
        self.ADCALineEdit.setText(attrs1[4])
        self.APCLineEdit.setText(attrs1[5])
        self.APCALineEdit.setText(attrs1[6])
        self.CRITCLineEdit.setText(attrs1[7])

        self.SPEEDLineEdit.setText(attrs2[0])
        self.WDEFLineEdit.setText(attrs2[1])
        self.WDEFALineEdit.setText(attrs2[2])
        self.MDEFLineEdit.setText(attrs2[3])
        self.MDEFALineEdit.setText(attrs2[4])

        self.HEALTHLineEdit.setText(attrs3[0])
        self.RECOVERLineEdit.setText(attrs3[1])
        self.RECOVERALineEdit.setText(attrs3[2])
        self.SHIELDLineEdit.setText(attrs3[3])
        self.SHIELDRECOVERLineEdit.setText(attrs3[4])
        self.SHIELDRECOVERALineEdit.setText(attrs3[5])

        self.CRITLineEdit.setText(attrs4[0])
        self.SKILLLineEdit.setText(attrs4[1])
        self.REFLECTLineEdit.setText(attrs4[2])
        self.VAMPIRELineEdit.setText(attrs4[3])

        for i in self.comboBoxList:
            if i.ATTR in attrs5:
                i.setChecked(True)
            else:
                i.setChecked(False)

    def rightMenuCreat(self):
        self.contextMenu = QMenu(self)
        self.cardImport = self.contextMenu.addAction(u'全部导入')
        self.cardImport.triggered.connect(lambda: self.cardImportFun())

        self.cardOutput = self.contextMenu.addAction(u'全部导出')
        self.cardOutput.triggered.connect(lambda: self.cardOutputFun())

    def rightMenuShow(self):
        self.contextMenu.popup(QCursor.pos())  # 2菜单显示的位置
        self.contextMenu.show()

    def wishImportFun(self, text=None):
        if text is None:
            text, ok = QInputDialog.getMultiLineText(self, '导入祝福', 'WISH 开头')
            if not (ok and text):
                return

        text = text.split(" ")
        if text[0] != "WISH":
            return
        else:
            wishLevelList = []
            for i in range(len(text) - 1):
                try:
                    wishLevelList.append(int(text[i + 1]))
                except:
                    pass
            self.wishpanel.wishLevelList = wishLevelList.copy()

    def amuletImportFun(self, text=None):
        if text is None:
            text, ok = QInputDialog.getMultiLineText(self, '导入护符', 'AMULET 开头,ENDAMULET结束')
            if not (ok and text):
                return

        text = text.split(" ")
        if text[0] != "AMULET":
            return
        else:
            amuletLevelList = [0] * len(all_amulet["data"])
            for i in range(1, len(text) - 2, 2):
                try:
                    name = text[i]
                    level = text[i + 1]
                    for j in range(len(all_amulet["data"])):
                        if all_amulet["data"][j] == name:
                            amuletLevelList[j] = int(level)
                except:
                    pass
            self.amuletpanel.amuletLevelList = amuletLevelList.copy()

    def cardImportFun(self, text=None):
        if text is None:
            text, ok = QInputDialog.getMultiLineText(self, '导入卡片', '计算器格式')
            if not (ok and text):
                return

        text = re.sub(r"\n\n", "\n", text)
        text = text.split("\n")

        attrs1 = text[0].split()
        if (len(attrs1) == 2):
            cardtype, nickname = attrs1[0].split("_")
            for i in range(len(all_character['data'])):
                if cardtype == all_character['data'][i]:
                    self.cardtypecomboBox.setCurrentIndex(i)
                    break
            self.nicknamelineedit.setText(nickname)
            text.pop(0)
            attrs1 = text[0].split()

        if attrs1[0] == "WISH":
            self.wishImportFun(text[0])
            text.pop(0)
            attrs1 = text[0].split()

        if attrs1[0] == "AMULET":
            self.amuletImportFun(text[0])
            text.pop(0)
            attrs1 = text[0].split()

        self.ADLineEdit.setText(attrs1[0])
        self.APLineEdit.setText(attrs1[1])
        self.RTKLineEdit.setText(attrs1[2])
        self.ADCLineEdit.setText(attrs1[3])
        self.ADCALineEdit.setText(attrs1[4])
        self.APCLineEdit.setText(attrs1[5])
        self.APCALineEdit.setText(attrs1[6])
        self.CRITCLineEdit.setText(attrs1[7])

        attrs2 = text[1].split()
        self.SPEEDLineEdit.setText(attrs2[0])
        self.WDEFLineEdit.setText(attrs2[1])
        self.WDEFALineEdit.setText(attrs2[2])
        self.MDEFLineEdit.setText(attrs2[3])
        self.MDEFALineEdit.setText(attrs2[4])

        attrs3 = text[2].split()
        self.HEALTHLineEdit.setText(attrs3[0])
        self.RECOVERLineEdit.setText(attrs3[1])
        self.RECOVERALineEdit.setText(attrs3[2])
        self.SHIELDLineEdit.setText(attrs3[3])
        self.SHIELDRECOVERLineEdit.setText(attrs3[4])
        self.SHIELDRECOVERALineEdit.setText(attrs3[5])

        attrs4 = text[3].split()
        self.CRITLineEdit.setText(attrs4[0])
        self.SKILLLineEdit.setText(attrs4[1])
        self.REFLECTLineEdit.setText(attrs4[2])
        self.VAMPIRELineEdit.setText(attrs4[3])

        others = text[4].split()
        for i in range(len(self.comboBoxList)):
            self.comboBoxList[i].setChecked(False)
            for j in range(1,len(others)):
                if others[j] == self.comboBoxList[i].ATTR:
                    self.comboBoxList[i].setChecked(True)

    def cardOutputFun(self, text=None):
        if not self.ableCheck():
            return

        thiscard = self.makeMyCard()
        thistext = thiscard.make_gu_text()
        QInputDialog.getMultiLineText(self, '请复制', "", thistext)
コード例 #23
0
class Window(QMainWindow, Ui_MainWindow):
    """Mian window for managing textures. The window lists all the folders
    from which the textures are linked in a scene. It also lists
    all the textures file names linked in the scene.
    """

    # create the application object
    __app = Application()

    def __init__(self, parent=__app.getMainWindow()):
        super(Window, self).__init__(parent)
        self.setupUi(self)
        self.setWindowTitle("TexMan")

        self._folderItems = []  # tree widget items representing folders
        self._fileItems = []  # tree widget items representing file names
        self._contextMenu = None

        # which method to call for each context menu action
        self._contexMenuActions = {
            "Browse Folder": self._browseFolder,
            "Copy Text": self._copyText
            # add more actions here
        }

        # override the contex menu event
        self.treeWidget.contextMenuEvent = self._showContextMenu

        # show info label if parent doesn't exist
        if parent is None:
            self._showInfoLabel("This DCC application isn't supported")
        else:
            # populate the window with available textures
            self._populate()

    def _showInfoLabel(self, message):
        """Displays an info label if no texture found or DCC not supported.
        """
        self.infoLabel.show()
        self.infoLabel.setText(message)
        self.treeWidget.hide()

    def _populate(self):
        """Populates the window with available folder paths and texture
        file names
        """

        # get all the available textures
        allTextures = self.__app.getAllTextures()

        # show info label if textures not found
        if not allTextures:
            self._showInfoLabel("No textures found in the scene")

        for texClassName in allTextures:  # loop through all texture types
            texClassNameItem = QTreeWidgetItem(self.treeWidget)
            texClassNameItem.setText(0, texClassName)
            # loop through all folder paths
            for texPath in allTextures[texClassName]:
                texPathItem = QTreeWidgetItem(texClassNameItem)
                texPathItem.setText(0, texPath)
                texPathItem.setIcon(
                    0, QIcon(os.path.join(_iconPath, 'folder.png')))
                self._folderItems.append(texPathItem)
                # loop through all the texture files in this path
                for texFile in allTextures[texClassName][texPath]:
                    texFileItem = QTreeWidgetItem(texPathItem)
                    texFileItem.setText(0, texFile.getFilename())
                    if texFile.exists():
                        texFileItem.setIcon(
                            0, QIcon(os.path.join(_iconPath, "check.png")))
                    else:
                        texFileItem.setIcon(
                            0, QIcon(os.path.join(_iconPath, "cross.png")))
                    self._fileItems.append(texFileItem)
            self.treeWidget.addTopLevelItem(texClassNameItem)

    def _selectedItems(self):
        """Returns selected tree widget items
        """
        return self.treeWidget.selectedItems()

    def _showContextMenu(self, event):
        """Creates and shows a context menu
        """
        if not self._contextMenu:
            self._contextMenu = QMenu(self)
            for actName, method in self._contexMenuActions.items():
                action = self._contextMenu.addAction(actName)
                action.triggered.connect(method)
        self._contextMenu.popup(event.globalPos())

    def _browseFolder(self):
        """Opens the folder for selected tree widget item
        """
        selectedItem = self._selectedItems()
        if selectedItem:
            folderPath = ""
            selectedItem = selectedItem[0]
            # check if the selected item is a folder path
            # tree widget item cannot checked using 'in' operator due to a bug
            # in PySide2, using 'id' function
            if id(selectedItem) in map(id, self._folderItems):
                folderPath = selectedItem.text(0)
            elif id(selectedItem) in map(id, self._fileItems):
                folderPath = selectedItem.parent().text(0)

            if folderPath != "":
                subprocess.Popen("explorer %s" % os.path.normpath(folderPath))

    def _copyText(self):
        """Copies the selected item text to the clipboad
        """
        selectedItem = self._selectedItems()
        if selectedItem:
            selectedItem = selectedItem[0]
            # get the clipboard
            clipboard = QGuiApplication.clipboard()
            # set the text
            clipboard.setText(selectedItem.text(0))
コード例 #24
0
ファイル: qtgui.py プロジェクト: davethecipo/polibeepsync
class MainWindow(Ui_Form):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.appname = "poliBeePsync"
        self.settings_fname = 'pbs-settings.ini'
        self.data_fname = 'pbs.data'
        self.setupUi(self)
        self.w = QWidget()

        self.status_signal = MySignal()
        self.status_signal.sig.connect(self.update_status_bar)

        self.logging_signal = MySignal()
        self.logging_signal.sig.connect(self.myStream_message)
        logging_console_hdl = SignalLoggingHandler(self.logging_signal)
        logger.addHandler(logging_console_hdl)
        commonlogger.addHandler(logging_console_hdl)

        self.about_text()
        self.timer = QTimer(self)

        # settings_path is a string containing the path to settings
        self.settings_path = None
        # settings is a dictionary of settings
        self.settings = None
        # load_settings() sets settings_path and settings
        self.load_settings()
        self.load_data()

        self.timer.timeout.connect(self.syncfiles)
        self.timer.start(1000 * 60 * int(self.settings['UpdateEvery']))

        self.loginthread = LoginThread(self.user, self)
        self.loginthread.signal_error.sig.connect(self.update_status_bar)
        self.loginthread.signal_ok.sig.connect(self.update_status_bar)

        self.refreshcoursesthread = RefreshCoursesThread(self.user, self)
        self.refreshcoursesthread.dumpuser.sig.connect(self.dumpUser)
        self.refreshcoursesthread.newcourses.sig.connect(self.addtocoursesview)
        self.refreshcoursesthread.newcourses.sig.connect(self.syncnewcourses)
        self.refreshcoursesthread.removable.sig.connect(self.rmfromcoursesview)

        self.downloadthread = DownloadThread(self.user,
                                             self.settings['RootFolder'],
                                             self)
        self.downloadthread.dumpuser.sig.connect(self.dumpUser)
        self.downloadthread.download_signal.connect(
            self.update_course_download)
        self.downloadthread.initial_sizes.connect(self.setinizialsizes)
        self.downloadthread.date_signal.connect(self.update_file_localtime)

        self._window.userCode.setText(str(self.user.username))
        self._window.userCode.editingFinished.connect(self.setusercode)
        self._window.password.setText(self.user.password)
        self._window.password.editingFinished.connect(self.setpassword)
        self._window.trylogin.clicked.connect(self.testlogin)

        self._window.courses_model = CoursesListModel(self.user.
                                                      available_courses)
        self._window.coursesView.setModel(self._window.courses_model)
        self._resizeview()
        self._window.refreshCourses.clicked.connect(self.refreshcourses)

        self._window.syncNow.clicked.connect(self.syncfiles)

        if self.settings['SyncNewCourses'] == str(True):
            self._window.sync_new = Qt.Checked
        else:
            self._window.sync_new = Qt.Unchecked

        self._window.rootfolder.setText(self.settings['RootFolder'])
        self._window.rootfolder.textChanged.connect(self.rootfolderslot)

        self._window.addSyncNewCourses.setCheckState(self._window.sync_new)
        self._window.addSyncNewCourses.stateChanged.connect(self.syncnewslot)

        self._window.timerMinutes.setValue(int(self.settings['UpdateEvery']))
        self._window.timerMinutes.valueChanged.connect(self.updateminuteslot)

        self._window.changeRootFolder.clicked.connect(self.chooserootdir)
        self._window.version_label.setText("Current version: {}."
                                           .format(__version__))
        self._window.check_version.clicked.connect(self.checknewversion)

        self.trayIconMenu = QMenu()
        self.trayIcon = QSystemTrayIcon(self.icon, self.w)
        self.trayIcon.activated.connect(self._activate_traymenu)
        self.createTray()

    @Slot()
    def _resizeview(self, **kwargs):
        self._window.coursesView.setColumnWidth(3, 160)
        self._window.coursesView.resizeColumnToContents(1)
        self._window.coursesView.setColumnWidth(0, 320)

    def checknewversion(self):
        rawdata = requests.get('https://pypi.python.org/pypi/'
                               'poliBeePsync/json')
        latest = json.loads(rawdata.text)['info']['version']
        self._window.version_label.setTextFormat(Qt.RichText)
        self._window.version_label.setOpenExternalLinks(True)
        self._window.version_label.setLocale(QLocale(QLocale.English,
                                                     QLocale.UnitedStates))
        self._window.version_label.setScaledContents(True)
        self._window.version_label.setWordWrap(True)
        if latest != __version__:
            newtext = """<p>Current version: {}.<br>
Latest version: {}. </p>
<p>Visit <a
href='https://jacotsu.github.io/polibeepsync/dirhtml/index.html\
        #how-to-install-upgrade-remove'>here</a> to find out how to upgrade.
""".format(__version__, latest)
        else:
            newtext = "Current version: {} up-to-date.".format(__version__)
        self._window.version_label.setText(newtext)

    def _update_time(self, folder, file, path_list):
        logger.debug(f'inside {folder.name}')
        for path in path_list:
            logger.debug(f'namegoto: {path}')
            folder_dict = {'name': path}
            fakefolder = Folder(folder_dict)
            logger.debug(f'contained folders:  {folder.folders}')
            ind = folder.folders.index(fakefolder)
            goto = folder.folders[ind]
            self._update_time(goto, file, path_list)

        if file in folder.files:
            ind = folder.files.index(file)
            thisfile = folder.files[ind]
            thisfile.local_creation_time = file.local_creation_time

    @Slot(tuple)
    def update_file_localtime(self, data, **kwargs):
        course, coursefile, path = data
        rootpath = os.path.join(self.settings['RootFolder'],
                                course.save_folder_name)
        if path.startswith(rootpath):
            partial = path[len(rootpath):]
        path_list = filter(None, partial.split(os.path.sep))
        self._update_time(course.documents, coursefile, path_list)

    @Slot(Course)
    def update_course_download(self, course, **kwargs):
        if course in self.user.available_courses:
            updating = self.user.available_courses[course.name]
            updating.downloaded_size = course.downloaded_size
            row = self._window.courses_model.courses.index(updating)
            where = self._window.courses_model.index(row, 3)
            self._window.courses_model.dataChanged.emit(where, where)

    @Slot(Course)
    def setinizialsizes(self, course, **kwargs):
        if course in self.user.available_courses:
            updating = self.user.available_courses[course.name]
            updating.downloaded_size = course.downloaded_size
            updating.total_file_size = course.total_file_size
            row = self._window.courses_model.courses.index(updating)
            where = self._window.courses_model.index(row, 3)
            self._window.courses_model.dataChanged.emit(where, where)
            self.dumpUser()

    @Slot(list)
    def syncnewcourses(self, newlist):
        if self.settings['SyncNewCourses'] == 'True':
            for elem in newlist:
                elem.sync = True

    def load_settings(self):
        for path in [user_config_dir(self.appname),
                     user_data_dir(self.appname)]:
            try:
                os.makedirs(path, exist_ok=True)
            except OSError:
                logger.critical('OSError while calling os.makedirs.',
                                exc_info=True)
                logger.critical(f"I couldn't create {path}.\nStart"
                                " poliBeePsync with --debug "
                                "error to get more details.")
        self.settings_path = os.path.join(user_config_dir(self.appname),
                                          self.settings_fname)
        defaults = {
            'UpdateEvery': '60',
            'RootFolder': os.path.join(os.path.expanduser('~'), self.appname),
            'SyncNewCourses': 'False'
        }
        self.settings = filesettings.settingsFromFile(self.settings_path,
                                                      defaults)

    def load_data(self):
        try:
            with open(os.path.join(user_data_dir(self.appname),
                                   self.data_fname), 'rb') as f:
                self.user = pickle.load(f)
                self.user.password = keyring\
                        .get_password('beep.metid.polimi.it',
                                      self.user.username)
                logger.info("Data has been loaded successfully.")
        except (EOFError, pickle.PickleError):
            logger.error('Settings corrupted', exc_info=True)
            self.user = User('', '')

        except FileNotFoundError:
            logger.error('Settings file not found.')
            self.user = User('', '')
            logger.error("I couldn't find data in the"
                         " predefined directory. Ignore this"
                         "message if you're using poliBeePsync"
                         " for the first time.")

    @Slot(str)
    def update_status_bar(self, status):
        self._window.statusbar.showMessage(status)

    @Slot(int)
    def syncnewslot(self, state):
        if state == 2:
            self.settings['SyncNewCourses'] = 'True'
        else:
            self.settings['SyncNewCourses'] = 'False'
        filesettings.settingsToFile(self.settings, self.settings_path)

    @Slot(int)
    def updateminuteslot(self, minutes):
        self.settings['UpdateEvery'] = str(minutes)
        filesettings.settingsToFile(self.settings, self.settings_path)
        self.timer.start(1000 * 60 * int(self.settings['UpdateEvery']))

    @Slot(str)
    def rootfolderslot(self, path):
        self.settings['RootFolder'] = path
        filesettings.settingsToFile(self.settings, self.settings_path)

    @Slot()
    def chooserootdir(self):
        currentdir = self.settings['RootFolder']
        flags = QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly
        newroot = QFileDialog.getExistingDirectory(None,
                                                   "Open Directory",
                                                   currentdir, flags)
        if newroot != "" and str(newroot) != currentdir:
            self.settings['RootFolder'] = str(newroot)
            filesettings.settingsToFile(self.settings, self.settings_path)
            self._window.rootfolder.setText(newroot)
            # we delete the already present downloadthread and recreate it
            # because otherwise it uses the old download folder. I don't know
            # if there's a cleaner approach
            del self.downloadthread
            self.downloadthread = DownloadThread(self.user,
                                                 self.settings['RootFolder'],
                                                 self)
            self.downloadthread.dumpuser.sig.connect(self.dumpUser)
            self.dumpUser()

    @Slot()
    def setusercode(self):
        newcode = self._window.userCode.text()
        try:
            if len(newcode) == 8:
                self.user.username = newcode
                logger.info(f'User code changed to {newcode}.')
                keyring.set_password('beep.metid.polimi.it',
                                     self.user.username,
                                     self.user.password)
        except OSError:
            logger.critical("I couldn't save data to disk. Run"
                            " poliBeePsync with option --debug"
                            " error to get more details.")
            logger.error('OSError raised while trying to write the User'
                         'instance to disk.', exc_info=True)

    @Slot()
    def setpassword(self):
        newpass = self._window.password.text()
        self.user.password = newpass
        try:
            keyring.set_password('beep.metid.polimi.it',
                                 self.user.username,
                                 self.user.password)
            logger.info("Password changed.")
        except OSError:
            logger.critical("I couldn't save data to disk. Run"
                            " poliBeePsync with option --debug"
                            " error to get more details.")
            logger.error('OSError raised while trying to write the User'
                         'instance to disk.', exc_info=True)

    @Slot()
    def testlogin(self):
        if not self.loginthread.isRunning():
            self.loginthread.exiting = False
            self.loginthread.start()
            self.status_signal.sig.emit("Logging in, please wait.")

    @Slot(list)
    def addtocoursesview(self, addlist):
        for elem in addlist:
            self._window.courses_model.insertRows(0, 1, elem)

    @Slot(list)
    def rmfromcoursesview(self, removelist):
        for elem in removelist:
            index = self._window.courses_model.courses.index(elem)
            self._window.courses_model.removeRows(index, 1)

    @Slot()
    def dumpUser(self):
        # we don't use the message...
        with open(os.path.join(user_data_dir(self.appname),
                               self.data_fname), 'wb') as f:
            tmp_pw = self.user.password
            self.user.password = ''
            pickle.dump(self.user, f)
            self.user.password = tmp_pw

    @Slot()
    def refreshcourses(self):
        self.status_signal.sig.emit('Searching for online updates...'
                                'this may take a while.')
        if not self.loginthread.isRunning():
            self.loginthread.exiting = False
            self.loginthread.signal_ok.sig.connect(self.do_refreshcourses)
            self.loginthread.start()

    def do_refreshcourses(self):
        self.loginthread.signal_ok.sig.disconnect(self.do_refreshcourses)
        if not self.refreshcoursesthread.isRunning():
            self.refreshcoursesthread.start()

    @Slot()
    def syncfiles(self):
        # we delete the already present downloadthread and recreate it
        # because otherwise it uses the old download folder. I don't know
        # if there's a cleaner approach
        del self.downloadthread
        self.downloadthread = DownloadThread(self.user,
                                             self.settings['RootFolder'],
                                             self)
        self.downloadthread.dumpuser.sig.connect(self.dumpUser)

        self.refreshcoursesthread.finished.connect(self.do_syncfiles)
        self.refreshcourses()

    @Slot()
    def do_syncfiles(self):
        self.refreshcoursesthread.finished.disconnect(self.do_syncfiles)
        self.status_signal.sig.emit('Started syncing.')
        self.downloadthread.start()

    @Slot(str)
    def myStream_message(self, message):
        self._window.status.moveCursor(QTextCursor.End)
        self._window.status.insertPlainText(message + "\n")

    def restore_window(self):
        self._window.setWindowState(self.windowState() & ~Qt.WindowMinimized |
                                Qt.WindowActive)
        self._window.show()

    def createTray(self):
        restoreAction = QAction("&Restore", self, triggered=self.restore_window)
        quitAction = QAction("&Quit", self, triggered=qApp.quit)
        self.trayIconMenu.addAction(restoreAction)
        self.trayIconMenu.addAction(quitAction)
        self.trayIcon.setContextMenu(self.trayIconMenu)
        self.trayIcon.show()

    @Slot(str)
    def _activate_traymenu(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
            self.restore_window()
        else:
            self.trayIconMenu.activateWindow()
            self.trayIconMenu.popup(QCursor.pos())

    def closeEvent(self, event):
        self._window.hide()
        event.ignore()

    def about_text(self):
        self._window.label_3 = QLabel()
        self._window.label_3.setTextFormat(Qt.RichText)
        self._window.label_3.setOpenExternalLinks(True)
        self._window.label_3.setLocale(QLocale(QLocale.English,
                                               QLocale.UnitedStates))
        self._window.label_3.setScaledContents(True)
        self._window.label_3.setWordWrap(True)
        text = """
<html>
<head/>
<body>
  <p>poliBeePsync is a program written by Davide Olianas,
released under GNU GPLv3+.</p>
  <p>Feel free to contact me at <a
  href=\"mailto:[email protected]\">[email protected]</a> for
  suggestions and bug reports.</p>
  <p>More information is available on the
  <a href=\"http://www.davideolianas.com/polibeepsync\">
  <span style=\" text-decoration: underline; color:#0000ff;\">
  official website</span></a>.
  </p>
</body>
</html>
"""

        self._window.label_3.setText(QApplication.translate("Form", text,
                                                            None))
コード例 #25
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())
コード例 #26
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()
コード例 #27
0
class MainWindow(QMainWindow, Ui_JAL_MainWindow):
    def __init__(self, own_path, language):
        QMainWindow.__init__(self, None)
        self.setupUi(self)

        self.own_path = own_path
        self.currentLanguage = language
        self.current_index = None  # this is used in onOperationContextMenu() to track item for menu

        self.ledger = Ledger()
        self.downloader = QuoteDownloader()
        self.taxes = TaxesRus()
        self.statements = StatementLoader()
        self.backup = JalBackup(self, get_dbfilename(self.own_path))
        self.estimator = None

        self.actionImportSlipRU.setEnabled(
            dependency_present(['pyzbar', 'PIL']))

        self.actionAbout = QAction(text=g_tr('MainWindow', "About"),
                                   parent=self)
        self.MainMenu.addAction(self.actionAbout)

        self.langGroup = QActionGroup(self.menuLanguage)
        self.createLanguageMenu()

        self.statementGroup = QActionGroup(self.menuStatement)
        self.createStatementsImportMenu()

        # Operations view context menu
        self.contextMenu = QMenu(self.OperationsTableView)
        self.actionReconcile = QAction(text=g_tr('MainWindow', "Reconcile"),
                                       parent=self)
        self.actionCopy = QAction(text=g_tr('MainWindow', "Copy"), parent=self)
        self.actionDelete = QAction(text=g_tr('MainWindow', "Delete"),
                                    parent=self)
        self.contextMenu.addAction(self.actionReconcile)
        self.contextMenu.addSeparator()
        self.contextMenu.addAction(self.actionCopy)
        self.contextMenu.addAction(self.actionDelete)

        # Customize Status bar and logs
        self.NewLogEventLbl = QLabel(self)
        self.StatusBar.addWidget(self.NewLogEventLbl)
        self.Logs.setNotificationLabel(self.NewLogEventLbl)
        self.Logs.setFormatter(
            logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
        self.logger = logging.getLogger()
        self.logger.addHandler(self.Logs)
        log_level = os.environ.get('LOGLEVEL', 'INFO').upper()
        self.logger.setLevel(log_level)

        # Setup reports tab
        self.reports = Reports(self.ReportTableView, self.ReportTreeView)

        # Customize UI configuration
        self.balances_model = BalancesModel(self.BalancesTableView)
        self.BalancesTableView.setModel(self.balances_model)
        self.balances_model.configureView()

        self.holdings_model = HoldingsModel(self.HoldingsTableView)
        self.HoldingsTableView.setModel(self.holdings_model)
        self.holdings_model.configureView()
        self.HoldingsTableView.setContextMenuPolicy(Qt.CustomContextMenu)

        self.operations_model = OperationsModel(self.OperationsTableView)
        self.OperationsTableView.setModel(self.operations_model)
        self.operations_model.configureView()
        self.OperationsTableView.setContextMenuPolicy(Qt.CustomContextMenu)

        self.connect_signals_and_slots()

        self.NewOperationMenu = QMenu()
        for i in range(self.OperationsTabs.count()):
            if hasattr(self.OperationsTabs.widget(i), "isCustom"):
                self.OperationsTabs.widget(i).dbUpdated.connect(
                    self.ledger.rebuild)
                self.OperationsTabs.widget(i).dbUpdated.connect(
                    self.operations_model.refresh)
                self.NewOperationMenu.addAction(
                    self.OperationsTabs.widget(i).name,
                    partial(self.createOperation, i))
        self.NewOperationBtn.setMenu(self.NewOperationMenu)

        # Setup balance and holdings parameters
        self.BalanceDate.setDateTime(QDateTime.currentDateTime())
        self.BalancesCurrencyCombo.setIndex(
            JalSettings().getValue('BaseCurrency'))
        self.HoldingsDate.setDateTime(QDateTime.currentDateTime())
        self.HoldingsCurrencyCombo.setIndex(
            JalSettings().getValue('BaseCurrency'))

        self.OperationsTabs.setCurrentIndex(TransactionType.NA)
        self.OperationsTableView.selectRow(0)
        self.OnOperationsRangeChange(0)

    def connect_signals_and_slots(self):
        self.actionExit.triggered.connect(QApplication.instance().quit)
        self.actionAbout.triggered.connect(self.showAboutWindow)
        self.langGroup.triggered.connect(self.onLanguageChanged)
        self.statementGroup.triggered.connect(self.statements.load)
        self.actionReconcile.triggered.connect(
            self.reconcileAtCurrentOperation)
        self.action_Load_quotes.triggered.connect(
            partial(self.downloader.showQuoteDownloadDialog, self))
        self.actionImportSlipRU.triggered.connect(self.importSlip)
        self.actionBackup.triggered.connect(self.backup.create)
        self.actionRestore.triggered.connect(self.backup.restore)
        self.action_Re_build_Ledger.triggered.connect(
            partial(self.ledger.showRebuildDialog, self))
        self.actionAccountTypes.triggered.connect(
            partial(self.onDataDialog, "account_types"))
        self.actionAccounts.triggered.connect(
            partial(self.onDataDialog, "accounts"))
        self.actionAssets.triggered.connect(
            partial(self.onDataDialog, "assets"))
        self.actionPeers.triggered.connect(partial(self.onDataDialog,
                                                   "agents"))
        self.actionCategories.triggered.connect(
            partial(self.onDataDialog, "categories"))
        self.actionTags.triggered.connect(partial(self.onDataDialog, "tags"))
        self.actionCountries.triggered.connect(
            partial(self.onDataDialog, "countries"))
        self.actionQuotes.triggered.connect(
            partial(self.onDataDialog, "quotes"))
        self.PrepareTaxForms.triggered.connect(
            partial(self.taxes.showTaxesDialog, self))
        self.BalanceDate.dateChanged.connect(
            self.BalancesTableView.model().setDate)
        self.HoldingsDate.dateChanged.connect(
            self.HoldingsTableView.model().setDate)
        self.BalancesCurrencyCombo.changed.connect(
            self.BalancesTableView.model().setCurrency)
        self.BalancesTableView.doubleClicked.connect(self.OnBalanceDoubleClick)
        self.HoldingsCurrencyCombo.changed.connect(
            self.HoldingsTableView.model().setCurrency)
        self.ReportRangeCombo.currentIndexChanged.connect(
            self.onReportRangeChange)
        self.RunReportBtn.clicked.connect(self.onRunReport)
        self.SaveReportBtn.clicked.connect(self.reports.saveReport)
        self.ShowInactiveCheckBox.stateChanged.connect(
            self.BalancesTableView.model().toggleActive)
        self.DateRangeCombo.currentIndexChanged.connect(
            self.OnOperationsRangeChange)
        self.ChooseAccountBtn.changed.connect(
            self.OperationsTableView.model().setAccount)
        self.SearchString.editingFinished.connect(self.updateOperationsFilter)
        self.HoldingsTableView.customContextMenuRequested.connect(
            self.onHoldingsContextMenu)
        self.OperationsTableView.selectionModel().selectionChanged.connect(
            self.OnOperationChange)
        self.OperationsTableView.customContextMenuRequested.connect(
            self.onOperationContextMenu)
        self.DeleteOperationBtn.clicked.connect(self.deleteOperation)
        self.actionDelete.triggered.connect(self.deleteOperation)
        self.CopyOperationBtn.clicked.connect(self.copyOperation)
        self.actionCopy.triggered.connect(self.copyOperation)
        self.downloader.download_completed.connect(self.balances_model.update)
        self.downloader.download_completed.connect(self.holdings_model.update)
        self.statements.load_completed.connect(self.ledger.rebuild)
        self.ledger.updated.connect(self.balances_model.update)
        self.ledger.updated.connect(self.holdings_model.update)

    @Slot()
    def closeEvent(self, event):
        self.logger.removeHandler(
            self.Logs
        )  # Removing handler (but it doesn't prevent exception at exit)
        logging.raiseExceptions = False  # Silencing logging module exceptions

    def createLanguageMenu(self):
        langPath = self.own_path + "languages" + os.sep

        langDirectory = QDir(langPath)
        for language_file in langDirectory.entryList(['*.qm']):
            language_code = language_file.split('.')[0]
            language = QLocale.languageToString(
                QLocale(language_code).language())
            language_icon = QIcon(langPath + language_code + '.png')
            action = QAction(language_icon, language, self)
            action.setCheckable(True)
            action.setData(language_code)
            self.menuLanguage.addAction(action)
            self.langGroup.addAction(action)

    @Slot()
    def onLanguageChanged(self, action):
        language_code = action.data()
        if language_code != self.currentLanguage:
            JalSettings().setValue('Language',
                                   JalDB().get_language_id(language_code))
            QMessageBox().information(
                self, g_tr('MainWindow', "Restart required"),
                g_tr('MainWindow', "Language was changed to ") +
                QLocale.languageToString(QLocale(language_code).language()) +
                "\n" + g_tr(
                    'MainWindow',
                    "You should restart application to apply changes\n"
                    "Application will be terminated now"), QMessageBox.Ok)
            self.close()

    # Create import menu for all known statements based on self.statements.sources values
    def createStatementsImportMenu(self):
        for i, source in enumerate(self.statements.sources):
            if 'icon' in source:
                source_icon = QIcon(self.own_path + "img" + os.sep +
                                    source['icon'])
                action = QAction(source_icon, source['name'], self)
            else:
                action = QAction(source['name'], self)
            action.setData(i)
            self.menuStatement.addAction(action)
            self.statementGroup.addAction(action)

    @Slot()
    def showAboutWindow(self):
        about_box = QMessageBox(self)
        about_box.setAttribute(Qt.WA_DeleteOnClose)
        about_box.setWindowTitle(g_tr('MainWindow', "About"))
        title = g_tr(
            'MainWindow',
            "<h3>JAL</h3><p>Just Another Ledger, version {version}</p>".format(
                version=__version__))
        about_box.setText(title)
        about = g_tr(
            'MainWindow',
            "<p>More information, manuals and problem reports are at "
            "<a href=https://github.com/titov-vv/jal>github home page</a></p>"
            "<p>Questions, comments, donations: <a href=mailto:[email protected]>[email protected]</a></p>"
        )
        about_box.setInformativeText(about)
        about_box.show()

    @Slot()
    def OnBalanceDoubleClick(self, index):
        self.ChooseAccountBtn.account_id = index.model().getAccountId(
            index.row())

    @Slot()
    def onReportRangeChange(self, range_index):
        report_ranges = {
            0: lambda: (0, 0),
            1: ManipulateDate.Last3Months,
            2: ManipulateDate.RangeYTD,
            3: ManipulateDate.RangeThisYear,
            4: ManipulateDate.RangePreviousYear
        }
        begin, end = report_ranges[range_index]()
        self.ReportFromDate.setDateTime(QDateTime.fromSecsSinceEpoch(begin))
        self.ReportToDate.setDateTime(QDateTime.fromSecsSinceEpoch(end))

    @Slot()
    def onRunReport(self):
        types = {
            0: ReportType.IncomeSpending,
            1: ReportType.ProfitLoss,
            2: ReportType.Deals,
            3: ReportType.ByCategory
        }
        report_type = types[self.ReportTypeCombo.currentIndex()]
        begin = self.ReportFromDate.dateTime().toSecsSinceEpoch()
        end = self.ReportToDate.dateTime().toSecsSinceEpoch()
        group_dates = 1 if self.ReportGroupCheck.isChecked() else 0
        if report_type == ReportType.ByCategory:
            self.reports.runReport(report_type, begin, end,
                                   self.ReportCategoryEdit.selected_id,
                                   group_dates)
        else:
            self.reports.runReport(report_type, begin, end,
                                   self.ReportAccountBtn.account_id,
                                   group_dates)

    @Slot()
    def OnOperationsRangeChange(self, range_index):
        view_ranges = {
            0: ManipulateDate.startOfPreviousWeek,
            1: ManipulateDate.startOfPreviousMonth,
            2: ManipulateDate.startOfPreviousQuarter,
            3: ManipulateDate.startOfPreviousYear,
            4: lambda: 0
        }
        self.OperationsTableView.model().setDateRange(
            view_ranges[range_index]())

    @Slot()
    def importSlip(self):
        dialog = ImportSlipDialog(self)
        dialog.show()

    @Slot()
    def onHoldingsContextMenu(self, pos):
        index = self.HoldingsTableView.indexAt(pos)
        contextMenu = QMenu(self.HoldingsTableView)
        actionEstimateTax = QAction(text=g_tr('Ledger',
                                              "Estimate Russian Tax"),
                                    parent=self.HoldingsTableView)
        actionEstimateTax.triggered.connect(
            partial(self.estimateRussianTax,
                    self.HoldingsTableView.viewport().mapToGlobal(pos), index))
        contextMenu.addAction(actionEstimateTax)
        contextMenu.popup(self.HoldingsTableView.viewport().mapToGlobal(pos))

    @Slot()
    def estimateRussianTax(self, position, index):
        model = index.model()
        account, asset, asset_qty = model.get_data_for_tax(index)
        self.estimator = TaxEstimator(account, asset, asset_qty, position)
        if self.estimator.ready:
            self.estimator.open()

    @Slot()
    def OnOperationChange(self, selected, _deselected):
        self.checkForUncommittedChanges()

        if len(self.OperationsTableView.selectionModel().selectedRows()) != 1:
            self.OperationsTabs.setCurrentIndex(TransactionType.NA)
        else:
            idx = selected.indexes()
            if idx:
                selected_row = idx[0].row()
                operation_type, operation_id = self.OperationsTableView.model(
                ).get_operation(selected_row)
                self.OperationsTabs.setCurrentIndex(operation_type)
                self.OperationsTabs.widget(operation_type).setId(operation_id)

    @Slot()
    def checkForUncommittedChanges(self):
        for i in range(self.OperationsTabs.count()):
            if hasattr(self.OperationsTabs.widget(i),
                       "isCustom") and self.OperationsTabs.widget(i).modified:
                reply = QMessageBox().warning(
                    None, g_tr('MainWindow', "You have unsaved changes"),
                    self.OperationsTabs.widget(i).name +
                    g_tr('MainWindow',
                         " has uncommitted changes,\ndo you want to save it?"),
                    QMessageBox.Yes, QMessageBox.No)
                if reply == QMessageBox.Yes:
                    self.OperationsTabs.widget(i).saveChanges()
                else:
                    self.OperationsTabs.widget(i).revertChanges()

    @Slot()
    def onOperationContextMenu(self, pos):
        self.current_index = self.OperationsTableView.indexAt(pos)
        if len(self.OperationsTableView.selectionModel().selectedRows()) != 1:
            self.actionReconcile.setEnabled(False)
            self.actionCopy.setEnabled(False)
        else:
            self.actionReconcile.setEnabled(True)
            self.actionCopy.setEnabled(True)
        self.contextMenu.popup(
            self.OperationsTableView.viewport().mapToGlobal(pos))

    @Slot()
    def reconcileAtCurrentOperation(self):
        self.operations_model.reconcile_operation(self.current_index.row())

    @Slot()
    def deleteOperation(self):
        if QMessageBox().warning(
                None, g_tr('MainWindow', "Confirmation"),
                g_tr('MainWindow',
                     "Are you sure to delete selected transacion(s)?"),
                QMessageBox.Yes, QMessageBox.No) == QMessageBox.No:
            return
        rows = []
        for index in self.OperationsTableView.selectionModel().selectedRows():
            rows.append(index.row())
        self.operations_model.deleteRows(rows)
        self.ledger.rebuild()

    @Slot()
    def createOperation(self, operation_type):
        self.checkForUncommittedChanges()
        self.OperationsTabs.widget(operation_type).createNew(
            account_id=self.operations_model.getAccount())
        self.OperationsTabs.setCurrentIndex(operation_type)

    @Slot()
    def copyOperation(self):
        operation_type = self.OperationsTabs.currentIndex()
        if operation_type == TransactionType.NA:
            return
        self.checkForUncommittedChanges()
        self.OperationsTabs.widget(operation_type).copyNew()

    @Slot()
    def updateOperationsFilter(self):
        self.OperationsTableView.model().filterText(self.SearchString.text())

    @Slot()
    def onDataDialog(self, dlg_type):
        if dlg_type == "account_types":
            AccountTypeListDialog().exec_()
        elif dlg_type == "accounts":
            AccountListDialog().exec_()
        elif dlg_type == "assets":
            AssetListDialog().exec_()
        elif dlg_type == "agents":
            PeerListDialog().exec_()
        elif dlg_type == "categories":
            CategoryListDialog().exec_()
        elif dlg_type == "tags":
            TagsListDialog().exec_()
        elif dlg_type == "countries":
            CountryListDialog().exec_()
        elif dlg_type == "quotes":
            QuotesListDialog().exec_()
        else:
            assert False
コード例 #28
0
class FLines(QOpenGLWidget):
    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)
        self.num = 3  # 顶点数
        self.dia = 100  # 直径
        self.linewidth = 2  # 线宽
        self.r = 255
        self.g = 255
        self.b = 0
        self.xRot = 0.0
        self.yRot = 0.0
        self.zRot = 0.0
        self.show_coor = True  # 是否显示模型坐标轴
        # 右键菜单
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.rightMenu)
        self.contextMenu = QMenu(self)
        self.DD = self.contextMenu.addMenu('顶点个数')
        self.D4 = self.DD.addAction('4')
        self.D10 = self.DD.addAction('10')
        self.D20 = self.DD.addAction('20')
        self.ZJ = self.contextMenu.addMenu('直径')
        self.ZJ50 = self.ZJ.addAction('50')
        self.ZJ150 = self.ZJ.addAction('150')
        self.ZJ200 = self.ZJ.addAction('200')
        self.YS = self.contextMenu.addMenu('颜色')
        self.YS1 = self.YS.addAction('绿色')
        self.YS2 = self.YS.addAction('黑色')
        self.YS3 = self.YS.addAction('白色')
        self.D4.triggered.connect(lambda: self.changeNum(4))
        self.D10.triggered.connect(lambda: self.changeNum(10))
        self.D20.triggered.connect(lambda: self.changeNum(20))
        self.ZJ50.triggered.connect(lambda: self.changeDia(50))
        self.ZJ150.triggered.connect(lambda: self.changeDia(150))
        self.ZJ200.triggered.connect(lambda: self.changeDia(200))
        self.YS1.triggered.connect(lambda: self.changeRGB([0, 255, 0]))
        self.YS2.triggered.connect(lambda: self.changeRGB([0, 0, 0]))
        self.YS3.triggered.connect(lambda: self.changeRGB([255, 255, 255]))
        # 快捷键
        QShortcut(QKeySequence(Qt.Key_F1), self, lambda: self.RotF('F1'))
        QShortcut(QKeySequence(Qt.Key_F2), self, lambda: self.RotF('F2'))
        QShortcut(QKeySequence(Qt.Key_F3), self, lambda: self.RotF('F3'))
        QShortcut(QKeySequence(Qt.Key_F4), self, lambda: self.RotF('F4'))

    def rightMenu(self):
        self.contextMenu.popup(QCursor.pos(), None)  # 菜单显示的位置

    def initializeGL(self):
        glClearColor(0.3, 0.3, 0.6, 1)  # 背景色

    def paintGL(self):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)  # 用当前背景色清屏
        glPushMatrix()
        glRotatef(self.xRot, 1.0, 0.0, 0.0)
        glRotatef(self.yRot, 0.0, 1.0, 0.0)
        glRotatef(self.zRot, 0.0, 0.0, 1.0)

        glLineWidth(self.linewidth)  # 设置线宽

        # 圆弧等分点
        theta = 2 * pi / self.num
        angle = 0
        z = 0
        Point = namedtuple('Point', ['x', 'y', 'z'])
        points_list = []
        for i in range(self.num):
            x = self.dia / 2 * cos(angle)
            y = self.dia / 2 * sin(angle)
            angle += theta
            p = Point(x, y, z)
            points_list.append(p)

        glColor3ub(self.r, self.g, self.b)  # 画笔色
        glBegin(GL_LINES)
        points_list2 = points_list[:]
        for i1 in points_list:
            points_list2.pop(0)
            for i in points_list2:
                glVertex3f(i1.x, i1.y, i1.z)
                glVertex3f(i.x, i.y, i.z)
        glEnd()

        if self.show_coor:  # 绘制模型坐标系
            glBegin(GL_LINES)
            # 以红色绘制x轴
            glColor4f(1.0, 0.0, 0.0, 1.0)  # 设置当前颜色为红色不透明
            glVertex3f(0.0, 0.0, 0.0)  # 设置x轴顶点(x轴负方向)
            glVertex3f(20, 0.0, 0.0)  # 设置x轴顶点(x轴正方向)
            # 以绿色绘制y轴
            glColor4f(0.0, 1.0, 0.0, 1.0)  # 设置当前颜色为绿色不透明
            glVertex3f(0.0, 0.0, 0.0)  # 设置y轴顶点(y轴负方向)
            glVertex3f(0.0, 20, 0.0)  # 设置y轴顶点(y轴正方向)
            # 以蓝色绘制z轴
            glColor4f(0.0, 0.0, 1.0, 1.0)  # 设置当前颜色为蓝色不透明
            glVertex3f(0.0, 0.0, 0.0)  # 设置z轴顶点(z轴负方向)
            glVertex3f(0.0, 0.0, 20)  # 设置z轴顶点(z轴正方向)
            glEnd()  # 结束绘制线段

            glPointSize(6)  # 设置点大小
            glBegin(GL_POINTS)  # 画点表示坐标轴正方向
            glColor4f(1.0, 0.0, 0.0, 1.0)
            glVertex3f(20, 0, 0)
            glColor4f(0.0, 1.0, 0.0, 1.0)
            glVertex3f(0, 20, 0)
            glColor4f(0.0, 0.0, 1.0, 1.0)
            glVertex3f(0, 0, 20)
            glEnd()

        glPopMatrix()  # 弹出矩阵

    def resizeGL(self, w: int, h: int):
        nRange = 100.0
        glViewport(0, 0, w, h)  # 设置视区和窗口等大
        glMatrixMode(GL_PROJECTION)  # 投影坐标系
        glLoadIdentity()  # 单位矩阵
        if w <= h:  # 平行投影
            glOrtho(-nRange, nRange, -nRange * h / w, nRange * h / w, -nRange,
                    nRange)
        else:
            glOrtho(-nRange * w / h, nRange * w / h, -nRange, nRange, -nRange,
                    nRange)
        glMatrixMode(GL_MODELVIEW)  # 模型坐标系
        glLoadIdentity()

    def changeNum(self, val):
        self.num = val
        self.update()

    def changeDia(self, val):
        self.dia = val
        self.update()

    def changeWidth(self, val):
        self.linewidth = val
        self.update()

    def changeR(self, val):
        self.r = val
        self.update()

    def changeG(self, val):
        self.g = val
        self.update()

    def changeB(self, val):
        self.b = val
        self.update()

    def changeRGB(self, RGB):
        self.r = RGB[0]
        self.g = RGB[1]
        self.b = RGB[2]
        self.update()

    def RotX(self, val):
        self.xRot = val
        self.update()

    def RotY(self, val):
        self.yRot = val
        self.update()

    def RotZ(self, val):
        self.zRot = val
        self.update()

    def RotF(self, F):
        if F == 'F1':
            self.xRot += 5
        elif F == 'F2':
            self.xRot -= 5
        elif F == 'F3':
            self.yRot += 5
        elif F == 'F4':
            self.yRot -= 5
        self.update()

    def showcoor(self, val):
        self.show_coor = val
        self.update()

    def wheelEvent(self, event):
        angle = event.angleDelta().y() / 8
        self.dia += angle
        if self.dia <= 50:
            self.dia = 50
        self.update()
コード例 #29
0
class SpineDatapackageWidget(QMainWindow):
    """A widget to edit CSV files in a Data Connection and create a tabular datapackage.
    """

    msg = Signal(str)
    msg_error = Signal(str)

    def __init__(self, datapackage):
        """Initialize class.

        Args:
            datapackage (CustomPackage): Data package associated to this widget
        """
        from ..ui.spine_datapackage_form import Ui_MainWindow  # pylint: disable=import-outside-toplevel

        super().__init__(flags=Qt.Window)
        self.datapackage = datapackage
        self.selected_resource_index = None
        self.resources_model = DatapackageResourcesModel(self, self.datapackage)
        self.fields_model = DatapackageFieldsModel(self, self.datapackage)
        self.foreign_keys_model = DatapackageForeignKeysModel(self, self.datapackage)
        self.resource_data_model = DatapackageResourceDataModel(self, self.datapackage)
        self.default_row_height = QFontMetrics(QFont("", 0)).lineSpacing()
        max_screen_height = max([s.availableSize().height() for s in QGuiApplication.screens()])
        self.visible_rows = int(max_screen_height / self.default_row_height)
        self.err_msg = QErrorMessage(self)
        self.notification_stack = NotificationStack(self)
        self._foreign_keys_context_menu = QMenu(self)
        self._file_watcher = QFileSystemWatcher(self)
        self._file_watcher.addPath(self.datapackage.base_path)
        self._changed_source_indexes = set()
        self.undo_group = QUndoGroup(self)
        self.undo_stacks = {}
        self._save_resource_actions = []
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.takeCentralWidget()
        self._before_save_all = self.ui.menuFile.insertSeparator(self.ui.actionSave_All)
        self.setWindowIcon(QIcon(":/symbols/app.ico"))
        self.qsettings = QSettings("SpineProject", "Spine Toolbox")
        self.restore_ui()
        self.add_menu_actions()
        self.setStyleSheet(MAINWINDOW_SS)
        self.ui.tableView_resources.setModel(self.resources_model)
        self.ui.tableView_resources.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_resource_data.setModel(self.resource_data_model)
        self.ui.tableView_resource_data.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_resource_data.horizontalHeader().setResizeContentsPrecision(self.visible_rows)
        self.ui.tableView_fields.setModel(self.fields_model)
        self.ui.tableView_fields.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_fields.horizontalHeader().setResizeContentsPrecision(self.visible_rows)
        self.ui.tableView_foreign_keys.setModel(self.foreign_keys_model)
        self.ui.tableView_foreign_keys.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_foreign_keys.horizontalHeader().setResizeContentsPrecision(self.visible_rows)
        self.connect_signals()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setWindowTitle("{0}[*] - Spine datapackage manager".format(self.datapackage.base_path))
        self.load_datapackage()

    @property
    def undo_stack(self):
        return self.undo_group.activeStack()

    @property
    def datapackage_path(self):
        return os.path.join(self.datapackage.base_path, "datapackage.json")

    def load_datapackage(self):
        self._file_watcher.addPaths(self.datapackage.sources)
        self.append_save_resource_actions()
        self.resources_model.refresh_model()
        first_index = self.resources_model.index(0, 0)
        if not first_index.isValid():
            return
        self.ui.tableView_resources.selectionModel().setCurrentIndex(first_index, QItemSelectionModel.Select)

    def add_menu_actions(self):
        """Add extra menu actions."""
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_resources.toggleViewAction())
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_data.toggleViewAction())
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_fields.toggleViewAction())
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_foreign_keys.toggleViewAction())
        undo_action = self.undo_group.createUndoAction(self)
        redo_action = self.undo_group.createRedoAction(self)
        undo_action.setShortcuts(QKeySequence.Undo)
        redo_action.setShortcuts(QKeySequence.Redo)
        undo_action.setIcon(QIcon(":/icons/menu_icons/undo.svg"))
        redo_action.setIcon(QIcon(":/icons/menu_icons/redo.svg"))
        before = self.ui.menuEdit.actions()[0]
        self.ui.menuEdit.insertAction(before, undo_action)
        self.ui.menuEdit.insertAction(before, redo_action)
        self.ui.menuEdit.insertSeparator(before)

    def connect_signals(self):
        """Connect signals to slots."""
        self.msg.connect(self.add_message)
        self.msg_error.connect(self.add_error_message)
        self._file_watcher.directoryChanged.connect(self._handle_source_dir_changed)
        self._file_watcher.fileChanged.connect(self._handle_source_file_changed)
        self.ui.actionCopy.triggered.connect(self.copy)
        self.ui.actionPaste.triggered.connect(self.paste)
        self.ui.actionClose.triggered.connect(self.close)
        self.ui.actionSave_All.triggered.connect(self.save_all)
        self.ui.actionSave_datapackage.triggered.connect(self.save_datapackage)
        self.ui.menuEdit.aboutToShow.connect(self.refresh_copy_paste_actions)
        self.fields_model.dataChanged.connect(self._handle_fields_data_changed)
        self.undo_group.cleanChanged.connect(self.update_window_modified)
        checkbox_delegate = CheckBoxDelegate(self)
        checkbox_delegate.data_committed.connect(self.fields_model.setData)
        self.ui.tableView_fields.setItemDelegateForColumn(2, checkbox_delegate)
        foreign_keys_delegate = ForeignKeysDelegate(self)
        foreign_keys_delegate.data_committed.connect(self.foreign_keys_model.setData)
        self.ui.tableView_foreign_keys.setItemDelegate(foreign_keys_delegate)
        self.ui.tableView_resources.selectionModel().currentChanged.connect(self._handle_current_resource_changed)
        self.ui.tableView_foreign_keys.customContextMenuRequested.connect(self.show_foreign_keys_context_menu)
        self._foreign_keys_context_menu.addAction("Remove foreign key", self._remove_foreign_key)

    @Slot(bool)
    def update_window_modified(self, _clean=None):
        """Updates window modified status and save actions depending on the state of the undo stack."""
        try:
            dirty_resource_indexes = {
                idx for idx in range(len(self.datapackage.resources)) if self.is_resource_dirty(idx)
            }
            dirty = bool(dirty_resource_indexes)
            self.setWindowModified(dirty)
        except RuntimeError:
            return
        self.ui.actionSave_datapackage.setEnabled(dirty)
        self.ui.actionSave_All.setEnabled(dirty)
        for idx, action in enumerate(self._save_resource_actions):
            dirty = idx in dirty_resource_indexes
            action.setEnabled(dirty)
            self.resources_model.update_resource_dirty(idx, dirty)

    def is_resource_dirty(self, resource_index):
        if resource_index in self._changed_source_indexes:
            return True
        try:
            return not self.undo_stacks[resource_index].isClean()
        except KeyError:
            return False

    def get_undo_stack(self, resource_index):
        if resource_index not in self.undo_stacks:
            self.undo_stacks[resource_index] = stack = QUndoStack(self.undo_group)
            stack.cleanChanged.connect(self.update_window_modified)
        return self.undo_stacks[resource_index]

    @Slot(str)
    def _handle_source_dir_changed(self, _path):
        if not self.datapackage.resources:
            self.load_datapackage()
            return
        self.datapackage.difference_infer(os.path.join(self.datapackage.base_path, '*.csv'))
        self._file_watcher.addPaths(self.datapackage.sources)
        self.append_save_resource_actions()
        self.resources_model.refresh_model()
        self.refresh_models()

    @Slot(str)
    def _handle_source_file_changed(self, path):
        for idx, source in enumerate(self.datapackage.sources):
            if os.path.normpath(source) == os.path.normpath(path):
                self._changed_source_indexes.add(idx)
                self.update_window_modified()
                break

    def append_save_resource_actions(self):
        new_actions = []
        for resource_index in range(len(self._save_resource_actions), len(self.datapackage.resources)):
            resource = self.datapackage.resources[resource_index]
            action = QAction(f"Save '{os.path.basename(resource.source)}'")
            action.setEnabled(False)
            action.triggered.connect(
                lambda checked=False, resource_index=resource_index: self.save_resource(resource_index)
            )
            new_actions.append(action)
        self.ui.menuFile.insertActions(self._before_save_all, new_actions)
        self._save_resource_actions += new_actions

    @Slot()
    def refresh_copy_paste_actions(self):
        """Adjusts copy and paste actions depending on which widget has the focus.
        """
        self.ui.actionCopy.setEnabled(focused_widget_has_callable(self, "copy"))
        self.ui.actionPaste.setEnabled(focused_widget_has_callable(self, "paste"))

    @Slot(str)
    def add_message(self, msg):
        """Prepend regular message to status bar.

        Args:
            msg (str): String to show in QStatusBar
        """
        self.notification_stack.push(msg)

    @Slot(str)
    def add_error_message(self, msg):
        """Show error message.

        Args:
            msg (str): String to show
        """
        self.err_msg.showMessage(msg)

    @Slot(bool)
    def save_all(self, _=False):
        resource_paths = {k: r.source for k, r in enumerate(self.datapackage.resources) if self.is_resource_dirty(k)}
        all_paths = list(resource_paths.values()) + [self.datapackage_path]
        if not self.get_permission(*all_paths):
            return
        for k, path in resource_paths.items():
            self._save_resource(k, path)
        self.save_datapackage()

    @Slot(bool)
    def save_datapackage(self, _=False):
        if self.datapackage.save(self.datapackage_path):
            self.msg.emit("'datapackage.json' succesfully saved")
            return
        self.msg_error.emit("Failed to save 'datapackage.json'")

    def save_resource(self, resource_index):
        resource = self.datapackage.resources[resource_index]
        filepath = resource.source
        if not self.get_permission(filepath, self.datapackage_path):
            return
        self._save_resource(resource_index, filepath)
        self.save_datapackage()

    def _save_resource(self, resource_index, filepath):
        headers = self.datapackage.resources[resource_index].schema.field_names
        self._file_watcher.removePath(filepath)
        with open(filepath, 'w', newline='') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(headers)
            for row in self.datapackage.resource_data(resource_index):
                writer.writerow(row)
        self.msg.emit(f"'{os.path.basename(filepath)}' succesfully saved")
        self._file_watcher.addPath(filepath)
        self._changed_source_indexes.discard(resource_index)
        stack = self.undo_stacks.get(resource_index)
        if not stack or stack.isClean():
            self.update_window_modified()
        elif stack:
            stack.setClean()

    def get_permission(self, *filepaths):
        start_dir = self.datapackage.base_path
        filepaths = [os.path.relpath(path, start_dir) for path in filepaths if os.path.isfile(path)]
        if not filepaths:
            return True
        pathlist = "".join([f"<li>{path}</li>" for path in filepaths])
        msg = f"The following file(s) in <b>{os.path.basename(start_dir)}</b> will be replaced: <ul>{pathlist}</ul>. Are you sure?"
        message_box = QMessageBox(
            QMessageBox.Question, "Replacing file(s)", msg, QMessageBox.Ok | QMessageBox.Cancel, parent=self
        )
        message_box.button(QMessageBox.Ok).setText("Replace")
        return message_box.exec_() != QMessageBox.Cancel

    @Slot(bool)
    def copy(self, checked=False):
        """Copies data to clipboard."""
        call_on_focused_widget(self, "copy")

    @Slot(bool)
    def paste(self, checked=False):
        """Pastes data from clipboard."""
        call_on_focused_widget(self, "paste")

    @Slot("QModelIndex", "QModelIndex")
    def _handle_current_resource_changed(self, current, _previous):
        """Resets resource data and schema models whenever a new resource is selected."""
        self.refresh_models(current)

    def refresh_models(self, current=None):
        if current is None:
            current = self.ui.tableView_resources.selectionModel().currentIndex()
        if current.column() != 0 or current.row() == self.selected_resource_index:
            return
        self.selected_resource_index = current.row()
        self.get_undo_stack(self.selected_resource_index).setActive()
        self.resource_data_model.refresh_model(self.selected_resource_index)
        self.fields_model.refresh_model(self.selected_resource_index)
        self.foreign_keys_model.refresh_model(self.selected_resource_index)
        self.ui.tableView_resource_data.resizeColumnsToContents()
        self.ui.tableView_fields.resizeColumnsToContents()
        self.ui.tableView_foreign_keys.resizeColumnsToContents()

    @Slot("QModelIndex", "QModelIndex", "QVector<int>")
    def _handle_fields_data_changed(self, top_left, bottom_right, roles):
        top, left = top_left.row(), top_left.column()
        bottom, right = bottom_right.row(), bottom_right.column()
        if left <= 0 <= right and Qt.DisplayRole in roles:
            # Fields name changed
            self.resource_data_model.headerDataChanged.emit(Qt.Horizontal, top, bottom)
            self.ui.tableView_resource_data.resizeColumnsToContents()
            self.foreign_keys_model.emit_data_changed()

    @Slot("QPoint")
    def show_foreign_keys_context_menu(self, pos):
        index = self.ui.tableView_foreign_keys.indexAt(pos)
        if not index.isValid() or index.row() == index.model().rowCount() - 1:
            return
        global_pos = self.ui.tableView_foreign_keys.viewport().mapToGlobal(pos)
        self._foreign_keys_context_menu.popup(global_pos)

    @Slot(bool)
    def _remove_foreign_key(self, checked=False):
        index = self.ui.tableView_foreign_keys.currentIndex()
        if not index.isValid():
            return
        index.model().call_remove_foreign_key(index.row())

    def restore_ui(self):
        """Restore UI state from previous session."""
        window_size = self.qsettings.value("dataPackageWidget/windowSize")
        window_pos = self.qsettings.value("dataPackageWidget/windowPosition")
        window_maximized = self.qsettings.value("dataPackageWidget/windowMaximized", defaultValue='false')
        window_state = self.qsettings.value("dataPackageWidget/windowState")
        n_screens = self.qsettings.value("mainWindow/n_screens", defaultValue=1)
        original_size = self.size()
        if window_size:
            self.resize(window_size)
        if window_pos:
            self.move(window_pos)
        # noinspection PyArgumentList
        if len(QGuiApplication.screens()) < int(n_screens):
            # There are less screens available now than on previous application startup
            self.move(0, 0)  # Move this widget to primary screen position (0,0)
        ensure_window_is_on_screen(self, original_size)
        if window_maximized == 'true':
            self.setWindowState(Qt.WindowMaximized)
        if window_state:
            self.restoreState(window_state, version=1)  # Toolbar and dockWidget positions

    def closeEvent(self, event=None):
        """Handle close event.

        Args:
            event (QEvent): Closing event if 'X' is clicked.
        """
        # save qsettings
        self.qsettings.setValue("dataPackageWidget/windowSize", self.size())
        self.qsettings.setValue("dataPackageWidget/windowPosition", self.pos())
        self.qsettings.setValue("dataPackageWidget/windowState", self.saveState(version=1))
        self.qsettings.setValue("dataPackageWidget/windowMaximized", self.windowState() == Qt.WindowMaximized)
        self.qsettings.setValue("dataPackageWidget/n_screens", len(QGuiApplication.screens()))
        if event:
            event.accept()
コード例 #30
0
 def on_custom_context_menu_requested(self):
     menu = QMenu(self)
     for action in self.actions:
         menu.addAction(action)
     menu.popup(self.cursor().pos())
コード例 #31
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__()))