def __init__(self,
                 parent: 'ElectrumWindow',
                 config: 'SimpleConfig',
                 go_tab=None):
        WindowModalDialog.__init__(self, parent, _('Preferences'))
        self.config = config
        self.window = parent
        self.need_restart = False
        self.fx = self.window.fx
        self.wallet = self.window.wallet

        vbox = QVBoxLayout()
        tabs = QTabWidget()
        tabs.setObjectName("settings_tab")
        gui_widgets = []
        tx_widgets = []
        oa_widgets = []

        # language
        lang_help = _(
            'Select which language is used in the GUI (after restart).')
        lang_label = HelpLabel(_('Language') + ':', lang_help)
        lang_combo = QComboBox()
        lang_combo.addItems(list(languages.values()))
        lang_keys = list(languages.keys())
        lang_cur_setting = self.config.get("language", '')
        try:
            index = lang_keys.index(lang_cur_setting)
        except ValueError:  # not in list
            index = 0
        lang_combo.setCurrentIndex(index)
        if not self.config.is_modifiable('language'):
            for w in [lang_combo, lang_label]:
                w.setEnabled(False)

        def on_lang(x):
            lang_request = list(languages.keys())[lang_combo.currentIndex()]
            if lang_request != self.config.get('language'):
                self.config.set_key("language", lang_request, True)
                self.need_restart = True

        lang_combo.currentIndexChanged.connect(on_lang)
        gui_widgets.append((lang_label, lang_combo))

        nz_help = _(
            'Number of zeros displayed after the decimal point. For example, if this is set to 2, "1." will be displayed as "1.00"'
        )
        nz_label = HelpLabel(_('Zeros after decimal point') + ':', nz_help)
        nz = QSpinBox()
        nz.setMinimum(0)
        nz.setMaximum(self.config.decimal_point)
        nz.setValue(self.config.num_zeros)
        if not self.config.is_modifiable('num_zeros'):
            for w in [nz, nz_label]:
                w.setEnabled(False)

        def on_nz():
            value = nz.value()
            if self.config.num_zeros != value:
                self.config.num_zeros = value
                self.config.set_key('num_zeros', value, True)
                self.window.history_list.update()
                self.window.address_list.update()

        nz.valueChanged.connect(on_nz)
        gui_widgets.append((nz_label, nz))

        msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
              + _('The following alias providers are available:') + '\n'\
              + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
              + 'For more information, see https://openalias.org'
        alias_label = HelpLabel(_('OpenAlias') + ':', msg)
        alias = self.config.get('alias', '')
        self.alias_e = QLineEdit(alias)
        self.set_alias_color()
        self.alias_e.editingFinished.connect(self.on_alias_edit)
        oa_widgets.append((alias_label, self.alias_e))

        # units
        units = base_units_list
        msg = (
            _('Base unit of your wallet.') +
            '\n1 Zcash = 1000 mZcash. 1 mZcash = 1000 uZcash. 1 uZcash = 100 satoshis.\n'
            +
            _('This setting affects the Send tab, and all balance related fields.'
              ))
        unit_label = HelpLabel(_('Base unit') + ':', msg)
        unit_combo = QComboBox()
        unit_combo.addItems(units)
        unit_combo.setCurrentIndex(units.index(self.window.base_unit()))

        def on_unit(x, nz):
            unit_result = units[unit_combo.currentIndex()]
            if self.window.base_unit() == unit_result:
                return
            edits = self.window.amount_e, self.window.receive_amount_e
            amounts = [edit.get_amount() for edit in edits]
            self.config.set_base_unit(unit_result)
            nz.setMaximum(self.config.decimal_point)
            self.window.update_tabs()
            for edit, amount in zip(edits, amounts):
                edit.setAmount(amount)
            self.window.update_status()

        unit_combo.currentIndexChanged.connect(lambda x: on_unit(x, nz))
        gui_widgets.append((unit_label, unit_combo))

        system_cameras = qrscanner._find_system_cameras()
        qr_combo = QComboBox()
        qr_combo.addItem("Default", "default")
        for camera, device in system_cameras.items():
            qr_combo.addItem(camera, device)
        #combo.addItem("Manually specify a device", config.get("video_device"))
        index = qr_combo.findData(self.config.get("video_device"))
        qr_combo.setCurrentIndex(index)
        msg = _("Install the zbar package to enable this.")
        qr_label = HelpLabel(_('Video Device') + ':', msg)
        qr_combo.setEnabled(qrscanner.libzbar is not None)
        on_video_device = lambda x: self.config.set_key(
            "video_device", qr_combo.itemData(x), True)
        qr_combo.currentIndexChanged.connect(on_video_device)
        gui_widgets.append((qr_label, qr_combo))

        colortheme_combo = QComboBox()
        colortheme_combo.addItem(_('Light'), 'default')
        colortheme_combo.addItem(_('Dark'), 'dark')
        index = colortheme_combo.findData(
            self.config.get('qt_gui_color_theme', 'default'))
        colortheme_combo.setCurrentIndex(index)
        colortheme_label = QLabel(_('Color theme') + ':')

        def on_colortheme(x):
            self.config.set_key('qt_gui_color_theme',
                                colortheme_combo.itemData(x), True)
            self.need_restart = True

        colortheme_combo.currentIndexChanged.connect(on_colortheme)
        gui_widgets.append((colortheme_label, colortheme_combo))

        show_utxo_time_cb = QCheckBox(_('Show UTXO timestamp/islock time'))
        show_utxo_time_cb.setChecked(self.config.get('show_utxo_time', False))

        def on_show_utxo_time_changed(x):
            show_utxo_time = (x == Qt.Checked)
            self.config.set_key('show_utxo_time', show_utxo_time, True)
            self.window.utxo_list.update()

        show_utxo_time_cb.stateChanged.connect(on_show_utxo_time_changed)
        gui_widgets.append((show_utxo_time_cb, None))

        updatecheck_cb = QCheckBox(
            _("Automatically check for software updates"))
        updatecheck_cb.setChecked(bool(self.config.get('check_updates',
                                                       False)))

        def on_set_updatecheck(v):
            self.config.set_key('check_updates', v == Qt.Checked, save=True)

        updatecheck_cb.stateChanged.connect(on_set_updatecheck)
        gui_widgets.append((updatecheck_cb, None))

        watchonly_w_cb = QCheckBox(_('Show warning for watching only wallets'))
        watchonly_w_cb.setChecked(self.config.get('watch_only_warn', True))

        def on_set_watch_only_warn(v):
            self.config.set_key('watch_only_warn', v == Qt.Checked, save=True)

        watchonly_w_cb.stateChanged.connect(on_set_watch_only_warn)
        gui_widgets.append((watchonly_w_cb, None))

        filelogging_cb = QCheckBox(_("Write logs to file"))
        filelogging_cb.setChecked(bool(self.config.get('log_to_file', False)))

        def on_set_filelogging(v):
            self.config.set_key('log_to_file', v == Qt.Checked, save=True)
            self.need_restart = True

        filelogging_cb.stateChanged.connect(on_set_filelogging)
        filelogging_cb.setToolTip(
            _('Debug logs can be persisted to disk. These are useful for troubleshooting.'
              ))
        gui_widgets.append((filelogging_cb, None))

        preview_cb = QCheckBox(_('Advanced preview'))
        preview_cb.setChecked(bool(self.config.get('advanced_preview', False)))
        preview_cb.setToolTip(
            _("Open advanced transaction preview dialog when 'Pay' is clicked."
              ))

        def on_preview(x):
            self.config.set_key('advanced_preview', x == Qt.Checked)

        preview_cb.stateChanged.connect(on_preview)
        tx_widgets.append((preview_cb, None))

        usechange_cb = QCheckBox(_('Use change addresses'))
        usechange_cb.setChecked(self.window.wallet.use_change)
        if not self.config.is_modifiable('use_change'):
            usechange_cb.setEnabled(False)

        def on_usechange(x):
            usechange_result = x == Qt.Checked
            if self.window.wallet.use_change != usechange_result:
                self.window.wallet.use_change = usechange_result
                self.window.wallet.db.put('use_change',
                                          self.window.wallet.use_change)
                multiple_cb.setEnabled(self.window.wallet.use_change)

        usechange_cb.stateChanged.connect(on_usechange)
        usechange_cb.setToolTip(
            _('Using change addresses makes it more difficult for other people to track your transactions.'
              ))
        tx_widgets.append((usechange_cb, None))

        def on_multiple(x):
            multiple = x == Qt.Checked
            if self.wallet.multiple_change != multiple:
                self.wallet.multiple_change = multiple
                self.wallet.db.put('multiple_change', multiple)

        multiple_change = self.wallet.multiple_change
        multiple_cb = QCheckBox(_('Use multiple change addresses'))
        multiple_cb.setEnabled(self.wallet.use_change)
        multiple_cb.setToolTip('\n'.join([
            _('In some cases, use up to 3 change addresses in order to break '
              'up large coin amounts and obfuscate the recipient address.'),
            _('This may result in higher transactions fees.')
        ]))
        multiple_cb.setChecked(multiple_change)
        multiple_cb.stateChanged.connect(on_multiple)
        tx_widgets.append((multiple_cb, None))

        def fmt_docs(key, klass):
            lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")]
            return '\n'.join([key, "", " ".join(lines)])

        choosers = sorted(coinchooser.COIN_CHOOSERS.keys())
        if len(choosers) > 1:
            chooser_name = coinchooser.get_name(self.config)
            msg = _(
                'Choose coin (UTXO) selection method.  The following are available:\n\n'
            )
            msg += '\n\n'.join(
                fmt_docs(*item) for item in coinchooser.COIN_CHOOSERS.items())
            chooser_label = HelpLabel(_('Coin selection') + ':', msg)
            chooser_combo = QComboBox()
            chooser_combo.addItems(choosers)
            i = choosers.index(chooser_name) if chooser_name in choosers else 0
            chooser_combo.setCurrentIndex(i)

            def on_chooser(x):
                chooser_name = choosers[chooser_combo.currentIndex()]
                self.config.set_key('coin_chooser', chooser_name)

            chooser_combo.currentIndexChanged.connect(on_chooser)
            tx_widgets.append((chooser_label, chooser_combo))

        def on_unconf(x):
            self.config.set_key('confirmed_only', bool(x))

        conf_only = bool(self.config.get('confirmed_only', False))
        unconf_cb = QCheckBox(_('Spend only confirmed coins'))
        unconf_cb.setToolTip(_('Spend only confirmed inputs.'))
        unconf_cb.setChecked(conf_only)
        unconf_cb.stateChanged.connect(on_unconf)
        tx_widgets.append((unconf_cb, None))

        def on_outrounding(x):
            self.config.set_key('coin_chooser_output_rounding', bool(x))

        enable_outrounding = bool(
            self.config.get('coin_chooser_output_rounding', True))
        outrounding_cb = QCheckBox(_('Enable output value rounding'))
        outrounding_cb.setToolTip(
            _('Set the value of the change output so that it has similar precision to the other outputs.'
              ) + '\n' + _('This might improve your privacy somewhat.') +
            '\n' +
            _('If enabled, at most 100 satoshis might be lost due to this, per transaction.'
              ))
        outrounding_cb.setChecked(enable_outrounding)
        outrounding_cb.stateChanged.connect(on_outrounding)
        tx_widgets.append((outrounding_cb, None))

        block_explorers = sorted(util.block_explorer_info().keys())
        BLOCK_EX_CUSTOM_ITEM = _("Custom URL")
        if BLOCK_EX_CUSTOM_ITEM in block_explorers:  # malicious translation?
            block_explorers.remove(BLOCK_EX_CUSTOM_ITEM)
        block_explorers.append(BLOCK_EX_CUSTOM_ITEM)
        msg = _(
            'Choose which online block explorer to use for functions that open a web browser'
        )
        block_ex_label = HelpLabel(_('Online Block Explorer') + ':', msg)
        block_ex_combo = QComboBox()
        block_ex_custom_e = QLineEdit(
            self.config.get('block_explorer_custom') or '')
        block_ex_combo.addItems(block_explorers)
        block_ex_combo.setCurrentIndex(
            block_ex_combo.findText(
                util.block_explorer(self.config) or BLOCK_EX_CUSTOM_ITEM))

        def showhide_block_ex_custom_e():
            block_ex_custom_e.setVisible(
                block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM)

        showhide_block_ex_custom_e()

        def on_be_combo(x):
            if block_ex_combo.currentText() == BLOCK_EX_CUSTOM_ITEM:
                on_be_edit()
            else:
                be_result = block_explorers[block_ex_combo.currentIndex()]
                self.config.set_key('block_explorer_custom', None, False)
                self.config.set_key('block_explorer', be_result, True)
            showhide_block_ex_custom_e()

        block_ex_combo.currentIndexChanged.connect(on_be_combo)

        def on_be_edit():
            val = block_ex_custom_e.text()
            try:
                val = ast.literal_eval(val)  # to also accept tuples
            except:
                pass
            self.config.set_key('block_explorer_custom', val)

        block_ex_custom_e.editingFinished.connect(on_be_edit)
        block_ex_hbox = QHBoxLayout()
        block_ex_hbox.setContentsMargins(0, 0, 0, 0)
        block_ex_hbox.setSpacing(0)
        block_ex_hbox.addWidget(block_ex_combo)
        block_ex_hbox.addWidget(block_ex_custom_e)
        block_ex_hbox_w = QWidget()
        block_ex_hbox_w.setLayout(block_ex_hbox)
        tx_widgets.append((block_ex_label, block_ex_hbox_w))

        # Fiat Currency
        hist_checkbox = QCheckBox()
        hist_capgains_checkbox = QCheckBox()
        fiat_address_checkbox = QCheckBox()
        ccy_combo = QComboBox()
        ex_combo = QComboBox()

        def update_currencies():
            if not self.window.fx: return
            currencies = sorted(
                self.fx.get_currencies(self.fx.get_history_config()))
            ccy_combo.clear()
            ccy_combo.addItems([_('None')] + currencies)
            if self.fx.is_enabled():
                ccy_combo.setCurrentIndex(
                    ccy_combo.findText(self.fx.get_currency()))

        def update_history_cb():
            if not self.fx: return
            hist_checkbox.setChecked(self.fx.get_history_config())
            hist_checkbox.setEnabled(self.fx.is_enabled())

        def update_fiat_address_cb():
            if not self.fx: return
            fiat_address_checkbox.setChecked(self.fx.get_fiat_address_config())

        def update_history_capgains_cb():
            if not self.fx: return
            hist_capgains_checkbox.setChecked(
                self.fx.get_history_capital_gains_config())
            hist_capgains_checkbox.setEnabled(hist_checkbox.isChecked())

        def update_exchanges():
            if not self.fx: return
            b = self.fx.is_enabled()
            ex_combo.setEnabled(b)
            if b:
                h = self.fx.get_history_config()
                c = self.fx.get_currency()
                exchanges = self.fx.get_exchanges_by_ccy(c, h)
            else:
                exchanges = self.fx.get_exchanges_by_ccy('USD', False)
            ex_combo.blockSignals(True)
            ex_combo.clear()
            ex_combo.addItems(sorted(exchanges))
            ex_combo.setCurrentIndex(
                ex_combo.findText(self.fx.config_exchange()))
            ex_combo.blockSignals(False)

        def on_currency(hh):
            if not self.fx: return
            b = bool(ccy_combo.currentIndex())
            ccy = str(ccy_combo.currentText()) if b else None
            self.fx.set_enabled(b)
            if b and ccy != self.fx.ccy:
                self.fx.set_currency(ccy)
            update_history_cb()
            update_exchanges()
            self.window.update_fiat()

        def on_exchange(idx):
            exchange = str(ex_combo.currentText())
            if self.fx and self.fx.is_enabled(
            ) and exchange and exchange != self.fx.exchange.name():
                self.fx.set_exchange(exchange)

        def on_history(checked):
            if not self.fx: return
            self.fx.set_history_config(checked)
            update_exchanges()
            self.window.history_model.refresh('on_history')
            if self.fx.is_enabled() and checked:
                self.fx.trigger_update()
            update_history_capgains_cb()

        def on_history_capgains(checked):
            if not self.fx: return
            self.fx.set_history_capital_gains_config(checked)
            self.window.history_model.refresh('on_history_capgains')

        def on_fiat_address(checked):
            if not self.fx: return
            self.fx.set_fiat_address_config(checked)
            self.window.address_list.refresh_headers()
            self.window.address_list.update()

        update_currencies()
        update_history_cb()
        update_history_capgains_cb()
        update_fiat_address_cb()
        update_exchanges()
        ccy_combo.currentIndexChanged.connect(on_currency)
        hist_checkbox.stateChanged.connect(on_history)
        hist_capgains_checkbox.stateChanged.connect(on_history_capgains)
        fiat_address_checkbox.stateChanged.connect(on_fiat_address)
        ex_combo.currentIndexChanged.connect(on_exchange)

        fiat_widgets = []
        fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
        fiat_widgets.append((QLabel(_('Source')), ex_combo))
        fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
        fiat_widgets.append((QLabel(_('Show capital gains in history')),
                             hist_capgains_checkbox))
        fiat_widgets.append((QLabel(_('Show Fiat balance for addresses')),
                             fiat_address_checkbox))

        tabs_info = [
            (gui_widgets, _('General')),
            (tx_widgets, _('Transactions')),
            (fiat_widgets, _('Fiat')),
            (oa_widgets, _('OpenAlias')),
        ]
        for widgets, name in tabs_info:
            tab = QWidget()
            tab.setObjectName(name)
            tab_vbox = QVBoxLayout(tab)
            grid = QGridLayout()
            for a, b in widgets:
                i = grid.rowCount()
                if b:
                    if a:
                        grid.addWidget(a, i, 0)
                    grid.addWidget(b, i, 1)
                else:
                    grid.addWidget(a, i, 0, 1, 2)
            tab_vbox.addLayout(grid)
            tab_vbox.addStretch(1)
            tabs.addTab(tab, name)

        vbox.addWidget(tabs)
        vbox.addStretch(1)
        vbox.addLayout(Buttons(CloseButton(self)))
        self.setLayout(vbox)

        if go_tab is not None:
            go_tab_w = tabs.findChild(QWidget, go_tab)
            if go_tab_w:
                tabs.setCurrentWidget(go_tab_w)
Exemple #2
0
class TopLevel(QWidget):
    """
        Main window containing all GUI components.
    """

    #
    # Used to cleanup background worker thread(s) (on exit)
    #
    class Exit(QObject):
        exitClicked = pyqtSignal()

    def closeEvent(self, event):
        self.close_event.exitClicked.emit()

    def __init__(self):
        super().__init__()

        self.close_event = self.Exit()  # Thread cleanup on exit
        self.close_event.exitClicked.connect(self.stop_background_workers)

        # Configurable
        self.worker_check_period = 1  # seconds

        self.init_ui()

    def init_online_pred_tab(self):
        """
            Initializes UI elements in the "Online Predictions" tab

        :return: (QWidget) online_training_tab
        """
        online_pred_tab = QWidget()
        return online_pred_tab

    def init_ui(self):
        """
            Initializes the top-level tab widget and all sub tabs ("Data", "Training", "Testing")
        """
        self.setGeometry(0, 0, 1100, 800)
        self.setWindowTitle('Myo Tools')
        self.setObjectName("TopWidget")
        self.setStyleSheet("#TopWidget {background-color: white;}")

        #
        # Top-level layout
        #
        tools_layout = QVBoxLayout()
        self.tool_tabs = QTabWidget()

        # Fancy styling
        tab_widgets = self.tool_tabs.findChild(QStackedWidget)
        tab_widgets.setObjectName("TabWidgets")
        tools_layout.addWidget(self.tool_tabs)
        top_tabs = self.tool_tabs.findChild(QTabBar)
        top_tabs.setObjectName("TopTabs")
        self.tool_tabs.setStyleSheet(
            "QTabBar#TopTabs::tab {font-weight: bold; height:35px; width: 150px; border-radius: 3px; "
            "                   border: 2px solid #bbbbbb; background-color:#dddddd;}"
            "QStackedWidget#TabWidgets {background-color: #eeeeee;}")
        self.tool_tabs.currentChanged.connect(self.on_tab_changed)
        self.cur_index = 0

        self.data_tools_tab = DataTools(self.on_device_connected,
                                        self.on_device_disconnected,
                                        self.is_data_tools_open)
        self.online_training_tab = OnlineTraining(
            self.data_tools_tab.data_collected)
        self.online_pred_tab = OnlineTesting(
            self.data_tools_tab.data_collected)

        self.tool_tabs.addTab(self.data_tools_tab, "Data Collection")
        self.tool_tabs.addTab(self.online_training_tab, "Online Training")
        self.tool_tabs.addTab(self.online_pred_tab, "Online Predictions")

        self.setLayout(tools_layout)
        self.show()

    def is_data_tools_open(self):
        return self.cur_index == 0

    def on_device_connected(self, address, rssi, battery_level):
        """
            Called on user initiated connection

        :param address: MAC address of connected Myo device
        """
        self.online_pred_tab.device_connected(address, rssi, battery_level)
        self.online_training_tab.device_connected(address, rssi, battery_level)

    def on_device_disconnected(self, address):
        """
            Called on user initiated disconnect, or unexpected disconnect

        :param address: MAC address of disconnected Myo device
        """
        self.online_pred_tab.device_disconnected(address)
        self.online_training_tab.device_disconnected(address)

    def on_tab_changed(self, value):
        """
            Intercepts a user attempting to switch tabs (to ensure a valid tab switch is taking place)

            value: Desired tab index to switch to
        """
        if self.cur_index == value:
            return
        valid_switch = False

        #
        # Determine if we can switch
        #
        data_tool_idx = 0
        online_train_idx = 1
        online_pred_idx = 2

        if self.cur_index == data_tool_idx:
            #
            # Check for incomplete Myo search workers
            #
            waiting_on_search = False
            for worker in self.data_tools_tab.search_threads:
                if not worker.complete:
                    waiting_on_search = True
                    break

            if not waiting_on_search:

                #
                # Check for background data workers
                #

                # worker_running  = False
                # num_widgets     = self.data_tools_tab.ports_found.count()
                #
                # for idx in range(num_widgets):
                #     # Ignore port widgets (only interested in Myo device rows)
                #     list_widget = self.data_tools_tab.ports_found.item(idx)
                #     if hasattr(list_widget, "port_idx"):
                #         continue
                #
                #     myo_widget = self.data_tools_tab.ports_found.itemWidget(list_widget)
                #     if not (myo_widget.worker is None):
                #         if not myo_widget.worker.complete:
                #             worker_running = True
                #             break
                worker_running = False

                if not worker_running:

                    #
                    # Close the background video worker if appropriate
                    #

                    # if not self.data_tools_tab.gt_helper_open:
                    #     if not (self.data_tools_tab.gt_helper.worker is None):
                    #         self.data_tools_tab.gt_helper.stop_videos()
                    #
                    #         while not (self.data_tools_tab.gt_helper.worker.complete):
                    #             time.sleep(self.worker_check_period)
                    #
                    #     #
                    #     # IF we make it here, the switch is valid (for the case of the data tools tab)
                    #     #
                    #     valid_switch = True
                    # else:
                    #     self.warn_user("Please close GT Helper first.")

                    valid_switch = True

                else:
                    self.warn_user(
                        "Please close connection to Myo devices first.")
            else:
                self.warn_user(
                    "Please wait for Myo device search to complete first.")

        #
        # To control switching out of online training / testing
        #
        elif self.cur_index == online_train_idx:
            valid_switch = True
        elif self.cur_index == online_pred_idx:
            valid_switch = True

        if valid_switch:
            self.cur_index = value
        else:
            self.tool_tabs.setCurrentIndex(self.cur_index)

    def stop_background_workers(self):
        """
            This function is called on (user click-initiated) exit of the main window.
        """
        self.data_tools_tab.stop_data_tools_workers()

    def warn_user(self, message):
        """
            Generates a pop-up warning message

        :param message: The text to display
        """
        self.warning = QErrorMessage()
        self.warning.showMessage(message)
        self.warning.show()
Exemple #3
0
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.chunk_directory = Directory(
            "CHUNK", QIcon(Assets.get_asset_path("document_a4_locked.png")),
            None)
        self.mod_directory = Directory(
            "MOD", QIcon(Assets.get_asset_path("document_a4.png")), None)
        self.workspace = Workspace([self.mod_directory, self.chunk_directory],
                                   parent=self)
        self.workspace.fileOpened.connect(self.handle_workspace_file_opened)
        self.workspace.fileClosed.connect(self.handle_workspace_file_closed)
        self.workspace.fileActivated.connect(
            self.handle_workspace_file_activated)
        self.workspace.fileLoadError.connect(
            self.handle_workspace_file_load_error)
        self.init_actions()
        self.init_menu_bar()
        self.init_toolbar()
        self.setStatusBar(QStatusBar())
        self.setWindowTitle("MHW-Editor-Suite")
        self.init_file_tree(self.chunk_directory,
                            "Chunk directory",
                            self.open_chunk_directory_action,
                            filtered=True)
        self.init_file_tree(self.mod_directory, "Mod directory",
                            self.open_mod_directory_action)
        self.init_help()
        self.setCentralWidget(self.init_editor_tabs())
        self.load_settings()

    def closeEvent(self, event):
        self.write_settings()

    def load_settings(self):
        self.settings = AppSettings()
        with self.settings.main_window() as group:
            size = group.get("size", QSize(1000, 800))
            position = group.get("position", QPoint(300, 300))
        with self.settings.application() as group:
            chunk_directory = group.get("chunk_directory", None)
            mod_directory = group.get("mod_directory", None)
            lang = group.get("lang", None)
        with self.settings.import_export() as group:
            self.import_export_default_attrs = {
                key: group.get(key, "").split(";")
                for key in group.childKeys()
            }
        # apply settings
        self.resize(size)
        self.move(position)
        if chunk_directory:
            self.chunk_directory.set_path(chunk_directory)
        if mod_directory:
            self.mod_directory.set_path(mod_directory)
        if lang:
            self.handle_set_lang_action(lang)

    def write_settings(self):
        with self.settings.main_window() as group:
            group["size"] = self.size()
            group["position"] = self.pos()
        with self.settings.application() as group:
            group["chunk_directory"] = self.chunk_directory.path
            group["mod_directory"] = self.mod_directory.path
            group["lang"] = FilePluginRegistry.lang
        with self.settings.import_export() as group:
            for key, value in self.import_export_default_attrs.items():
                group[key] = ";".join(value)

    def get_icon(self, name):
        return self.style().standardIcon(name)

    def init_actions(self):
        self.open_chunk_directory_action = create_action(
            self.get_icon(QStyle.SP_DirOpenIcon), "Open chunk_directory ...",
            self.handle_open_chunk_directory, None)
        self.open_mod_directory_action = create_action(
            self.get_icon(QStyle.SP_DirOpenIcon), "Open mod directory ...",
            self.handle_open_mod_directory, QKeySequence.Open)
        self.save_file_action = create_action(
            self.get_icon(QStyle.SP_DriveHDIcon), "Save file",
            self.handle_save_file_action, QKeySequence.Save)
        self.save_file_action.setDisabled(True)
        self.export_action = create_action(self.get_icon(QStyle.SP_FileIcon),
                                           "Export file ...",
                                           self.handle_export_file_action)
        self.export_action.setDisabled(True)
        self.import_action = create_action(self.get_icon(QStyle.SP_FileIcon),
                                           "Import file ...",
                                           self.handle_import_file_action)
        self.import_action.setDisabled(True)
        self.help_action = create_action(None, "Show help",
                                         self.handle_show_help_action)
        self.about_action = create_action(None, "About",
                                          self.handle_about_action)
        self.lang_actions = {
            lang: create_action(None,
                                name,
                                partial(self.handle_set_lang_action, lang),
                                checkable=True)
            for lang, name in LANG
        }
        self.quick_access_actions = [
            create_action(
                None, title,
                partial(self.workspace.open_file_any_dir, file_rel_path))
            for title, file_rel_path in QUICK_ACCESS_ITEMS
        ]

    def init_menu_bar(self):
        menu_bar = self.menuBar()
        # file menu
        file_menu = menu_bar.addMenu("File")
        file_menu.insertAction(None, self.open_chunk_directory_action)
        file_menu.insertAction(None, self.open_mod_directory_action)
        file_menu.insertAction(None, self.export_action)
        file_menu.insertAction(None, self.import_action)
        file_menu.insertAction(None, self.save_file_action)

        quick_access_menu = menu_bar.addMenu("Quick Access")
        for action in self.quick_access_actions:
            quick_access_menu.insertAction(None, action)

        # lang menu
        lang_menu = menu_bar.addMenu("Language")
        for action in self.lang_actions.values():
            lang_menu.insertAction(None, action)

        # help menu
        help_menu = menu_bar.addMenu("Help")
        help_menu.insertAction(None, self.help_action)
        help_menu.insertAction(None, self.about_action)

    def init_toolbar(self):
        toolbar = self.addToolBar("Main")
        toolbar.setIconSize(QSize(16, 16))
        toolbar.setFloatable(False)
        toolbar.setMovable(False)
        toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        toolbar.insertAction(None, self.open_mod_directory_action)
        toolbar.insertAction(None, self.save_file_action)

    def init_file_tree(self, directory, title, action, filtered=False):
        widget = DirectoryDockWidget(directory, filtered=filtered, parent=self)
        widget.path_label.addAction(action, QLineEdit.LeadingPosition)
        widget.tree_view.activated.connect(
            partial(self.handle_directory_tree_view_activated, directory))
        dock = QDockWidget(title, self)
        dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        dock.setFeatures(QDockWidget.DockWidgetMovable)
        dock.setWidget(widget)
        self.addDockWidget(Qt.LeftDockWidgetArea, dock)

    def init_help(self):
        self.help_widget = HelpWidget(self)
        self.help_widget_dock = QDockWidget("Help", self)
        self.help_widget_dock.setAllowedAreas(Qt.LeftDockWidgetArea
                                              | Qt.RightDockWidgetArea)
        self.help_widget_dock.setFeatures(QDockWidget.DockWidgetMovable)
        self.help_widget_dock.setWidget(self.help_widget)
        self.addDockWidget(Qt.RightDockWidgetArea, self.help_widget_dock)
        self.help_widget_dock.hide()

    def handle_show_help_action(self):
        if self.help_widget_dock.isVisible():
            self.help_widget_dock.hide()
        else:
            self.help_widget_dock.show()

    def handle_directory_tree_view_activated(self, directory,
                                             qindex: QModelIndex):
        if qindex.model().isDir(qindex):
            return
        file_path = qindex.model().filePath(qindex)
        self.workspace.open_file(directory, file_path)

    def init_editor_tabs(self):
        self.editor_tabs = QTabWidget()
        self.editor_tabs.setDocumentMode(True)
        self.editor_tabs.setTabsClosable(True)
        self.editor_tabs.tabCloseRequested.connect(
            self.handle_editor_tab_close_requested)
        return self.editor_tabs

    def handle_workspace_file_opened(self, path, rel_path):
        ws_file = self.workspace.files[path]
        editor_view = EditorView.factory(self.editor_tabs, ws_file)
        editor_view.setObjectName(path)
        self.editor_tabs.addTab(editor_view, ws_file.directory.file_icon,
                                f"{ws_file.directory.name}: {rel_path}")
        self.editor_tabs.setCurrentWidget(editor_view)
        self.save_file_action.setDisabled(False)
        self.export_action.setDisabled(False)
        self.import_action.setDisabled(False)

    def handle_workspace_file_activated(self, path, rel_path):
        widget = self.editor_tabs.findChild(QWidget, path)
        self.editor_tabs.setCurrentWidget(widget)

    def handle_workspace_file_closed(self, path, rel_path):
        widget = self.editor_tabs.findChild(QWidget, path)
        widget.deleteLater()
        has_no_files_open = not self.workspace.files
        self.save_file_action.setDisabled(has_no_files_open)
        self.export_action.setDisabled(has_no_files_open)
        self.import_action.setDisabled(has_no_files_open)

    def handle_workspace_file_load_error(self, path, rel_path, error):
        QMessageBox.warning(self, f"Error loading file `{rel_path}`",
                            f"Error while loading\n{path}:\n\n{error}",
                            QMessageBox.Ok, QMessageBox.Ok)

    def handle_editor_tab_close_requested(self, tab_index):
        editor_view = self.editor_tabs.widget(tab_index)
        self.workspace.close_file(editor_view.workspace_file)

    def handle_open_chunk_directory(self):
        path = QFileDialog.getExistingDirectory(parent=self,
                                                caption="Open chunk directory")
        if path:
            self.chunk_directory.set_path(os.path.normpath(path))

    def handle_open_mod_directory(self):
        path = QFileDialog.getExistingDirectory(parent=self,
                                                caption="Open mod directory")
        if path:
            self.mod_directory.set_path(os.path.normpath(path))

    def handle_save_file_action(self):
        main_ws_file = self.get_current_workspace_file()
        for ws_file in main_ws_file.get_files_modified():
            if ws_file.directory is self.chunk_directory:
                if self.mod_directory.is_valid:
                    self.transfer_file_to_mod_workspace(
                        ws_file, ws_file is main_ws_file)
                else:
                    self.save_base_content_file(ws_file)
            else:
                with show_error_dialog(self, "Error writing file"):
                    self.save_workspace_file(ws_file)

    def handle_export_file_action(self):
        ws_file = self.get_current_workspace_file()
        plugin = FilePluginRegistry.get_plugin(ws_file.abs_path)
        fields = plugin.data_factory.EntryFactory.fields()
        data = [it.as_dict() for it in ws_file.data.entries]
        dialog = ExportDialog.init(self, data, fields,
                                   plugin.import_export.get("safe_attrs"))
        dialog.open()

    def handle_import_file_action(self):
        ws_file = self.get_current_workspace_file()
        plugin = FilePluginRegistry.get_plugin(ws_file.abs_path)
        fields = plugin.data_factory.EntryFactory.fields()
        dialog = ImportDialog.init(self,
                                   fields,
                                   plugin.import_export.get("safe_attrs"),
                                   as_list=True)
        if dialog:
            dialog.import_accepted.connect(self.handle_import_accepted)
            dialog.open()

    def handle_import_accepted(self, import_data):
        ws_file = self.get_current_workspace_file()
        num_items = min(len(import_data), len(ws_file.data))
        for idx in range(num_items):
            ws_file.data[idx].update(import_data[idx])
        self.statusBar().showMessage(
            f"Import contains {len(import_data)} items. "
            f"Model contains {len(ws_file.data)} items. "
            f"Imported {num_items}.", STATUSBAR_MESSAGE_TIMEOUT)

    def handle_set_lang_action(self, lang):
        FilePluginRegistry.lang = lang
        for act in self.lang_actions.values():
            act.setChecked(False)
        self.lang_actions[lang].setChecked(True)

    def get_current_workspace_file(self):
        editor = self.editor_tabs.currentWidget()
        return editor.workspace_file

    def save_base_content_file(self, ws_file):
        result = QMessageBox.question(
            self, "Save base content file?",
            "Do you really want to update this chunk file?",
            QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Cancel)
        if result == QMessageBox.Ok:
            with show_error_dialog(self, "Error writing file"):
                self.save_workspace_file(ws_file)

    def transfer_file_to_mod_workspace(self, ws_file, reopen=False):
        mod_abs_path, exists = self.mod_directory.get_child_path(
            ws_file.rel_path)
        if not exists:
            return self.transfer_file(ws_file, self.mod_directory, reopen)

        result = QMessageBox.question(
            self, "File exists, overwrite?",
            f"File '{ws_file.rel_path}' already found in mod directory, overwrite?",
            QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
        if result == QMessageBox.Ok:
            self.transfer_file(ws_file, self.mod_directory, reopen)

    def transfer_file(self, ws_file, target_directory, reopen=False):
        if target_directory is ws_file.directory:
            return
        self.workspace.close_file(ws_file)
        ws_file.set_directory(target_directory)
        self.save_workspace_file(ws_file)
        if reopen:
            self.workspace.open_file(target_directory, ws_file.abs_path)

    def save_workspace_file(self, ws_file):
        ws_file.save()
        self.statusBar().showMessage(f"File '{ws_file.abs_path}' saved.",
                                     STATUSBAR_MESSAGE_TIMEOUT)

    def handle_about_action(self):
        dialog = QDialog(self)
        dialog.setWindowTitle("About MHW Editor Suite")
        layout = QVBoxLayout()
        dialog.setLayout(layout)
        about_text = QLabel(ABOUT_TEXT)
        about_text.setTextFormat(Qt.RichText)
        about_text.setTextInteractionFlags(Qt.TextBrowserInteraction)
        about_text.setOpenExternalLinks(True)
        layout.addWidget(about_text)
        dialog.exec()
Exemple #4
0
class View(QWidget):
    """View component of the Model-View structure of the optimization window
    class"""

    #Class signals
    new_dir = pyqtSignal(str)
    get_params = pyqtSignal(str, list, str, str, str, str)
    gen_report = pyqtSignal(str, list, list)
    send_data = pyqtSignal(str, str)
    send_table = pyqtSignal(str, list)

    def __init__(self, portfolio_dic, dir_path):
        """Initializes the View component"""

        super(QWidget, self).__init__()
        self.layout = QVBoxLayout(self)
        self.portfolio_dic = portfolio_dic
        self.dir_path = dir_path
        self.tab_dic = {}
        self.weight_dic = {}
        self.selection = None
        self.fig_els = {
            key: {}
            for key in [key for key in self.portfolio_dic.keys()]
        }
        # Initialize tab screen
        self.tabs = QTabWidget()

        self.tabs.resize(300, 200)

        self.tabUI(state='init')

        # Add tabs to widget
        self.tabs.currentChanged.connect(self.current_tab)
        self.layout.addWidget(self.tabs)

        self.setLayout(self.layout)

        #Initializes the app GUI
    def tabUI(self, state='init'):
        """Initializes the tab structure of the GUI
        Very messy function, definitley needs to be refactored
        """

        #Initialization routine for the tab widget architecture
        #This section is run when the app is first opened
        if state == 'init':

            #Loops over the number of entries in portfolio_dic
            # and generates a separate tab for each entry
            for key, value in self.portfolio_dic.items():

                dic = {}

                #Initializes a tab widget and sets its name
                #to the corresponding portfolio name
                tab1 = QWidget()
                tab1.setObjectName(key)

                #Initializes a custom combobox for selecting frequency
                freq_combo_box = self.combo_boxUI()

                #Initializes a line edit box where tickers can be entered
                #The intial value is set to the value of the current entry
                #in portfolio_dic
                tick_box = QLineEdit()
                tick_box.setText(value)

                #Initializes a line edit widget where a file directory can be set
                tick_dir_box = QLineEdit('')
                tick_dir_box.setObjectName('TickBox')

                #If dir_path argument was passed to the Optimization_Window Class
                #This sets that path as the value of the tick_dir lineedit widget
                if self.dir_path:
                    tick_dir_box.setText(self.dir_path)

                #Initializes a button that allows the user to set a file directory through
                #a file explorer window
                set_dir_button = QPushButton('Set Directory')
                set_dir_button.setObjectName(key)
                set_dir_button.clicked.connect(self.change_directory)

                #Initializes a custom combobox widget that allows a user to set the number
                #of theoretical portfolios to be generated during optimization processes
                num_port_combo_box = self.combo_boxUI('Num_Ports')

                #Initializes custom combobox widgets tha allows a user to set
                #Boundary conditions for asset weights in their portfolios
                min_weight_box = self.combo_boxUI('MinW')
                max_weight_box = self.combo_boxUI('MaxW')

                # Initializes a line edit that accepts a risk free rate number
                rf_box = QLineEdit()
                rf_box.setText('2.43')

                #Initializes a button that commences optimization processes
                button = QPushButton('Optimize')
                button.setObjectName(key)
                button.clicked.connect(self.on_btn_clic)

                #Initializes a layout class object
                layout = QFormLayout()

                #Initialized widgets are added to the layout
                layout.addRow('Tickers', tick_box)
                layout.addRow('', QLabel(''))
                layout.addRow('Ticker Directory', tick_dir_box)
                layout.addRow('', set_dir_button)
                layout.addRow('', QLabel(''))
                layout.addRow('Select Frequency', freq_combo_box)
                layout.addRow('', QLabel(''))
                layout.addRow('Number of Portfolios', num_port_combo_box)
                layout.addRow('', QLabel(''))
                layout.addRow('Minimum Asset Proportion (%)', min_weight_box)
                layout.addRow('Maximum Asset Proportion (%)', max_weight_box)
                layout.addRow('', QLabel(''))
                layout.addRow('Risk Free Rate (%)', rf_box)
                layout.addRow('', button)

                #The tabs layout is set to the generated layout
                tab1.setLayout(layout)

                #The current tab is added to the parent tab widget
                self.tabs.addTab(tab1, str(key))

                #Generated widgets,widget parameters, the layout, and tab
                #are added to the tab_dic class attribute to be called later
                #for re rendering the GUI
                dic['Tab'] = tab1
                dic['Tickers'] = value
                dic['Layout'] = layout
                dic['Freq'] = freq_combo_box
                dic['Tick_box'] = tick_box
                dic['Dir_box'] = tick_dir_box
                dic['Dir_box_text'] = str(tick_dir_box.text())
                dic['Num_port'] = num_port_combo_box
                dic['Opt_button'] = button
                dic['MinWeight'] = min_weight_box
                dic['MaxWeight'] = max_weight_box
                dic['Rf_rate'] = rf_box
                self.tab_dic[key] = dic

        else:

            self.left = 100
            self.top = 100
            self.width = 1050
            self.height = 850
            self.setGeometry(self.left, self.top, self.width, self.height)

            dic = {}
            current = self.name
            for i in reversed(range(len(self.tab_dic))):
                self.tabs.removeTab(i)

            for key, val in self.tab_dic.items():
                #Loops over the number of entries in tab_dic
                #number of entries = number of tabs = number of passed portfolios

                dic[key] = {}
                #Initializes a blank tab widget
                tab1 = QWidget()
                tab1.setObjectName(key)

                #gets layout from tab_dic
                layout = val['Layout']

                #Sets the new tab's layout as the retrieved layout
                tab1.setLayout(layout)
                self.tabs.addTab(tab1, str(key))

                #updates the tab and layout values in tab_dic
                self.tab_dic[key]['Tab'] = tab1
                self.tab_dic[key]['Layout'] = layout

            self.tabs.setCurrentWidget(self.tabs.findChild(QWidget, current))

    def current_tab(self):
        """Stores the indexed name of the currently viewed tab"""
        index = self.tabs.currentIndex()
        name = self.tabs.tabText(index)
        self.name = name
        current_ticks = str(self.tab_dic[self.name]['Tick_box'].text())
        self.ticker_list = pmt.ticker_parse(current_ticks)

    def combo_selected(self, text):
        """Test function, can be deleted later"""
        print(text)

    def combo_boxUI(self, box_type='Freq'):
        """Initializes a CustomComboBox class object and returns it to 
        be added into a layout"""

        combo_box = pgw.CustomComboBox(box_type)
        combo_box.activated[str].connect(self.combo_selected)

        return combo_box

    def change_directory(self):
        """Opens a file explorer window, prompts user to select the directory
        from which the necessary ticker files are stored"""

        sending_button = self.sender()
        name = sending_button.objectName()
        self.new_dir.emit(name)

    def update_directory(self, dname, name):
        """Updates the shown directory in the directory line edit widget"""

        self.tab_dic[name]['Dir_box_text'] = dname
        self.tab_dic[name]['Dir_box'].setText(dname)
        # self.tabUI(state='update_directory')

        self.tabs.setCurrentWidget(self.tabs.findChild(QWidget, name))

    def build_report(self):
        """Emits a signal carrying name and tickers arguments to the 
        model component of the optimization window structure, a report
        generation function is then run by the model"""

        self.gen_report.emit(self.name, self.ticker_list, self.selection)

    def request_data(self, key, data_type):
        """Sends a signal to the model requesting data specified by 
        data_type and a key corresponding to the current selected tab"""

        self.send_data.emit(key, data_type)

    def get_data(self, data, data_type):
        """receives data from the model and stores it in a class attribute"""

        if data_type == 'Opt':
            self.opt_params = data
        else:
            self.sec_params = data

    def on_btn_clic(self):
        """Handles portfolio optimization and refreshes the relevant
        tab to reflect the results of the optimization process"""

        #Gets the tab in which an optimization button was pressed
        sending_button = self.sender()
        name = sending_button.objectName()

        #Clears the current tab layout
        self.rem_layout = self.tab_dic[name]['Layout']
        self.clear_tab_layout()

        #adds a progress Bar in the middle of the tab widget
        for i in range(7):
            self.rem_layout.addRow('', QLabel(""))
        self.progress = QProgressBar(self)
        self.rem_layout.addRow('          ', self.progress)

        #Optimization work, tickers are passed to the optimization worker
        #thread which handles optimization math and progress bar
        current_ticks = str(self.tab_dic[name]['Tick_box'].text())
        self.ticker_list = pmt.ticker_parse(current_ticks)

        #Arguments required to run the External worker thread
        num_portfolios = int(self.tab_dic[name]['Num_port'].currentText())
        min_b = int(self.tab_dic[name]['MinWeight'].currentText())
        max_b = int(self.tab_dic[name]['MaxWeight'].currentText())

        bounds = (min_b, max_b)

        #Initializes External worker thread which generates weight set perumutations while running a progress bar
        self.calc = External(self.ticker_list, num_portfolios, bounds, name)

        #Connects progress counter signal
        self.calc.countChanged.connect(self.onCountChanged)

        #Starts the thread worker
        self.calc.start()

        #Accepts signals from the worker thread when its job is completed
        self.calc.weights.connect(self.receive_weights)
        self.calc.finished.connect(self.onFinished)

    def onCountChanged(self, value):
        """sets value of progress bar, connected to the countchanged
        signal in the 'External' thread class object"""

        self.progress.setValue(value)

    def clear_tab_layout(self):
        """Clears all widgets and layouts from the current tab"""

        for i in reversed(range(self.rem_layout.count())):
            widgetToRemove = self.rem_layout.itemAt(i)

            if widgetToRemove.widget():
                wid = widgetToRemove.widget()
                self.rem_layout.removeWidget(wid)
                wid.setParent(None)

    def receive_weights(self, weights, name):
        """Stores weight permutation set generated and transmitted by External worker thread
        """ ""
        self.weight_dic[name] = weights

    def onFinished(self, fin, name):
        """This function is called when the optimization work is completed,
        the progress bar is cleared and data visualization widgets are 
        rendered on the tab widget"""

        if fin == 'Finished':

            #Ends worker thread
            self.calc.stop()

            #Clears progress bar widget from GUI
            self.progress.hide()

            #Clears the layout of the current tab
            self.clear_tab_layout()

            tickers = self.tab_dic[name]['Tick_box'].text()
            weights = self.weight_dic[name]
            dir_path = self.tab_dic[name]['Dir_box_text']
            freq = str(self.tab_dic[name]['Freq'].currentText())
            rf_rate = self.tab_dic[name]['Rf_rate'].text()

            #Generates portfolio parameters
            self.get_params.emit(tickers, weights, dir_path, freq, rf_rate,
                                 name)

            self.name = name

            #stores portfolio parameters in GUI data dictionary
            #intializes a graph widget to plot the generated portfolio data
            graphWidget = pgw.CustomCanvas(self.opt_params)

            self.tab_dic[name]['graph'] = graphWidget

            #Generates a new layout object
            layout = self.resultsUI(name)

            #Stores that layout in the tab architecture dictionary
            self.tab_dic[name]['Layout'] = layout

            #Renders the new tab layout
            self.tabUI(state='change')

            #Sets GUI to the current tab
            self.tabs.setCurrentWidget(self.tabs.findChild(QWidget, name))

    def param_gen(self, sec_params, opt_params, ticker_list):
        """Receives signal from model component carrying dictionaries
        containing data on the optimization process and the portfolio 
        securities"""
        print(opt_params)
        self.sec_params = sec_params
        self.opt_params = opt_params
        self.ticker_list = ticker_list

    def resultsUI(self, name):
        """Utilizes the results layout of the working tab after optimization processes have
        finished"""

        #Initializes layout widgets
        layout1a = QHBoxLayout()
        layout1 = QVBoxLayout()
        layout2 = QGridLayout()

        #Initializes label widgets
        label = QLabel('Plot Items')
        label2 = QLabel('')

        #Intializes checkbox widgets and connects functions to them
        chk1 = QCheckBox('Efficient Frontier')
        chk2 = QCheckBox('All Portfolios')
        chk2.setChecked(True)
        self.graph_box_checked(chk1, name)
        self.graph_box_checked(chk2, name)
        chk1.toggled.connect(lambda: self.graph_box_checked(chk1, name))
        chk2.toggled.connect(lambda: self.graph_box_checked(chk2, name))

        #Initializes button widgets and connects functions to them
        apply_button = QPushButton('Apply')
        report_button = QPushButton('Generate Report')
        report_button.clicked.connect(self.build_report)
        apply_button.setObjectName(name)
        apply_button.clicked.connect(self.apply_plot_settings)

        #Initializes a tableviewer for lasso selected portfolios
        tableview = QTableView()
        tableview.setSortingEnabled(True)
        if self.selection:
            print('self Selection exists')
            mod = pgw.TableModel(self.selection, self.ticker_list)
        else:
            mod = pgw.TableModel()
        tableview.setModel(mod)

        #Initializes a toolbar for navigating the rendered figure
        graphWidget = self.tab_dic[name]['graph']
        graph_tools = NavigationToolbar(graphWidget, self)

        #Initializes Widget wid1 sets layouts and adds child widgets
        wid1 = QWidget()
        layout1.addWidget(graphWidget)
        layout1.addWidget(QLabel(''))
        layout1.addWidget(graph_tools)
        wid1.setLayout(layout1)

        #Initalizes Widget wid1a sets layout and adds child widgets
        wid1a = QWidget()
        layout1a.addWidget(tableview)
        layout1a.addWidget(wid1)
        wid1a.setLayout(layout1a)

        #Initializes Widget wid2 sets layout and adds child widgets
        wid2 = QWidget()
        layout2.addWidget(label, 0, 0, 1, 1)
        layout2.addWidget(label2, 1, 0, 1, 1)
        layout2.addWidget(chk1, 2, 0, 1, 1)
        layout2.addWidget(chk2, 3, 0, 1, 1)
        layout2.addWidget(report_button, 3, 2, 1, 1)
        layout2.addWidget(apply_button, 2, 2, 1, 1)
        wid2.setLayout(layout2)

        #Initializes the main layout 'layout' and adds
        #The previously intializeds widgets to it
        layout = QVBoxLayout()
        layout.addWidget(wid1a)
        layout.addWidget(wid2)

        return layout

    def apply_plot_settings(self):
        """This function handles redrawing the central figure widget when
         new graph parameters are selected and applied from the GUI
         """

        #Gets button name for finding relevant entries in f_e dictionary
        sending_button = self.sender()
        name = sending_button.objectName()

        #Sets Figure Elements dictionary
        f_e = self.fig_els[name]

        #Gets items to be graphed from Figure Elements dictionary
        graph_items = [key for key, val in f_e.items() if val == True]

        #Gets optimization paramters dictionary
        self.request_data(name, 'Opt')
        opt_params = self.opt_params

        #Generates graph Widget
        graphWidget = pgw.CustomCanvas(opt_params, graph_items)
        self.tab_dic[name]['graph'] = graphWidget

        #Reconstructs the tab layout
        layout = self.resultsUI(name)
        self.tab_dic[name]['Layout'] = layout

        #Renders the GUI
        self.tabUI(state='change')
        self.tabs.setCurrentWidget(self.tabs.findChild(QWidget, self.name))

    def graph_box_checked(self, b, name):
        """Test function to check checkbox functionality, can be deleted at a later point"""

        if b.isChecked():
            self.fig_els[name][b.text()] = True
        else:
            self.fig_els[name][b.text()] = False

    def keyPressEvent(self, event):
        """This function handles all key events in the GUI"""
        #Ends program event loop
        if event.key() == Qt.Key_Q:
            self.deleteLater()

        #Returns the parameters of data points within lasso selection
        elif event.key() == Qt.Key_Return:
            graphWidget = self.tab_dic[self.name]['graph']
            self.lsso = graphWidget.CustomPlot.lsso
            self.request_table(self.name)

            f_e = self.fig_els[self.name]

            #Gets items to be graphed from Figure Elements dictionary
            graph_items = [key for key, val in f_e.items() if val == True]
            self.request_data(self.name, 'Opt')
            opt_params = self.opt_params

            graphWidget = pgw.CustomCanvas(opt_params, graph_items)
            self.tab_dic[self.name]['graph'] = graphWidget
            layout = self.resultsUI(self.name)
            self.tab_dic[self.name]['Layout'] = layout
            self.tabUI(state='change')

        event.accept()

    def request_table(self, name):
        """Sends selection data to model for processing"""
        sel = list(self.lsso.xys[self.lsso.ind])
        self.send_table.emit(name, sel)

    def update_table(self, selection):
        """Receives data from model used to update table values"""
        self.selection = selection
Exemple #5
0
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.chunk_directory = Directory(
            "CHUNK",
            QIcon(Assets.get_asset_path("document_a4_locked.png")),
            None)
        self.mod_directory = Directory(
            "MOD",
            QIcon(Assets.get_asset_path("document_a4.png")),
            None)
        self.workspace = Workspace([self.mod_directory, self.chunk_directory],
                                   parent=self)
        self.workspace.fileOpened.connect(self.handle_workspace_file_opened)
        self.workspace.fileClosed.connect(self.handle_workspace_file_closed)
        self.workspace.fileActivated.connect(self.handle_workspace_file_activated)
        self.workspace.fileLoadError.connect(self.handle_workspace_file_load_error)
        self.init_actions()
        self.init_menu_bar()
        self.init_toolbar()
        self.setStatusBar(QStatusBar())
        self.setWindowTitle("MHW-Editor-Suite")
        self.init_file_tree(
            self.chunk_directory, "Chunk directory",
            self.open_chunk_directory_action,
            filtered=True)
        self.init_file_tree(
            self.mod_directory,
            "Mod directory",
            self.open_mod_directory_action)
        self.setCentralWidget(self.init_editor_tabs())
        self.load_settings()

    def closeEvent(self, event):
        self.write_settings()

    def load_settings(self):
        self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope,
                                  "fre-sch.github.com",
                                  "MHW-Editor-Suite")
        self.settings.beginGroup("MainWindow")
        size = self.settings.value("size", QSize(1000, 800))
        position = self.settings.value("position", QPoint(300, 300))
        self.settings.endGroup()
        self.settings.beginGroup("Application")
        chunk_directory = self.settings.value("chunk_directory", None)
        mod_directory = self.settings.value("mod_directory", None)
        lang = self.settings.value("lang", None)
        self.settings.endGroup()
        self.resize(size)
        self.move(position)
        if chunk_directory:
            self.chunk_directory.set_path(chunk_directory)
        if mod_directory:
            self.mod_directory.set_path(mod_directory)
        if lang:
            self.handle_set_lang_action(lang)

    def write_settings(self):
        self.settings.beginGroup("MainWindow")
        self.settings.setValue("size", self.size())
        self.settings.setValue("position", self.pos())
        self.settings.endGroup()
        self.settings.beginGroup("Application")
        self.settings.setValue("chunk_directory", self.chunk_directory.path)
        self.settings.setValue("mod_directory", self.mod_directory.path)
        self.settings.setValue("lang", FilePluginRegistry.lang)
        self.settings.endGroup()

    def get_icon(self, name):
        return self.style().standardIcon(name)

    def init_actions(self):
        self.open_chunk_directory_action = create_action(
            self.get_icon(QStyle.SP_DirOpenIcon),
            "Open chunk_directory ...",
            self.handle_open_chunk_directory,
            None)
        self.open_mod_directory_action = create_action(
            self.get_icon(QStyle.SP_DirOpenIcon),
            "Open mod directory ...",
            self.handle_open_mod_directory,
            QKeySequence.Open)
        self.save_file_action = create_action(
            self.get_icon(QStyle.SP_DriveHDIcon),
            "Save file",
            self.handle_save_file_action,
            QKeySequence.Save)
        self.save_file_action.setDisabled(True)
        self.export_csv_action = create_action(
            self.get_icon(QStyle.SP_FileIcon),
            "Export file to CSV...",
            self.handle_export_file_action)
        self.export_csv_action.setDisabled(True)
        self.about_action = create_action(
            None, "About", self.handle_about_action)
        self.lang_actions = {
            lang: create_action(
                None, name, partial(self.handle_set_lang_action, lang),
                checkable=True)
            for lang, name in LANG
        }

    def init_menu_bar(self):
        menubar = self.menuBar()
        file_menu = menubar.addMenu("File")
        file_menu.insertAction(None, self.open_chunk_directory_action)
        file_menu.insertAction(None, self.open_mod_directory_action)
        file_menu.insertAction(None, self.export_csv_action)
        file_menu.insertAction(None, self.save_file_action)
        lang_menu = menubar.addMenu("Language")
        for action in self.lang_actions.values():
            lang_menu.insertAction(None, action)
        help_menu = menubar.addMenu("Help")
        help_menu.insertAction(None, self.about_action)

    def init_toolbar(self):
        toolbar = self.addToolBar("Main")
        toolbar.setIconSize(QSize(16, 16))
        toolbar.setFloatable(False)
        toolbar.setMovable(False)
        toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        toolbar.insertAction(None, self.open_mod_directory_action)
        toolbar.insertAction(None, self.save_file_action)

    def init_file_tree(self, directory, title, action, filtered=False):
        widget = DirectoryDockWidget(directory, filtered=filtered, parent=self)
        widget.path_label.addAction(action, QLineEdit.LeadingPosition)
        widget.tree_view.activated.connect(
            partial(self.handle_directory_tree_view_activated, directory))
        dock = QDockWidget(title, self)
        dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
        dock.setFeatures(QDockWidget.DockWidgetMovable)
        dock.setWidget(widget)
        self.addDockWidget(Qt.LeftDockWidgetArea, dock)

    def handle_directory_tree_view_activated(self, directory, qindex: QModelIndex):
        if qindex.model().isDir(qindex):
            return
        file_path = qindex.model().filePath(qindex)
        self.workspace.open_file(directory, file_path)

    def init_editor_tabs(self):
        self.editor_tabs = QTabWidget()
        self.editor_tabs.setDocumentMode(True)
        self.editor_tabs.setTabsClosable(True)
        self.editor_tabs.tabCloseRequested.connect(
            self.handle_editor_tab_close_requested)
        return self.editor_tabs

    def handle_workspace_file_opened(self, path, rel_path):
        ws_file = self.workspace.files[path]
        editor_view = EditorView.factory(self.editor_tabs, ws_file)
        editor_view.setObjectName(path)
        self.editor_tabs.addTab(editor_view,
                                ws_file.directory.file_icon,
                                f"{ws_file.directory.name}: {rel_path}")
        self.editor_tabs.setCurrentWidget(editor_view)
        self.save_file_action.setDisabled(False)
        self.export_csv_action.setDisabled(False)

    def handle_workspace_file_activated(self, path, rel_path):
        widget = self.editor_tabs.findChild(QWidget, path)
        self.editor_tabs.setCurrentWidget(widget)

    def handle_workspace_file_closed(self, path, rel_path):
        widget = self.editor_tabs.findChild(QWidget, path)
        widget.deleteLater()
        self.save_file_action.setDisabled(not self.workspace.files)
        self.export_csv_action.setDisabled(not self.workspace.files)

    def handle_workspace_file_load_error(self, path, rel_path, error):
        QMessageBox.warning(self, f"Error loading file `{rel_path}`",
                            f"Error while loading\n{path}:\n\n{error}",
                            QMessageBox.Ok, QMessageBox.Ok)

    def handle_editor_tab_close_requested(self, tab_index):
        editor_view = self.editor_tabs.widget(tab_index)
        self.workspace.close_file(editor_view.workspace_file)

    def handle_open_chunk_directory(self):
        path = QFileDialog.getExistingDirectory(parent=self,
                                                caption="Open chunk directory")
        if path:
            self.chunk_directory.set_path(os.path.normpath(path))

    def handle_open_mod_directory(self):
        path = QFileDialog.getExistingDirectory(parent=self,
                                                caption="Open mod directory")
        if path:
            self.mod_directory.set_path(os.path.normpath(path))

    def handle_save_file_action(self):
        editor = self.editor_tabs.currentWidget()
        main_ws_file = editor.workspace_file
        for ws_file in main_ws_file.get_files_modified():
            if ws_file.directory is self.chunk_directory:
                if self.mod_directory.is_valid:
                    self.transfer_file_to_mod_workspace(
                        ws_file, ws_file is main_ws_file)
                else:
                    self.save_base_content_file(ws_file)
            else:
                with show_error_dialog(self, "Error writing file"):
                    self.save_workspace_file(ws_file)

    def handle_export_file_action(self):
        editor = self.editor_tabs.currentWidget()
        ws_file = editor.workspace_file
        file_name, file_type = QFileDialog.getSaveFileName(self, "Export file as CSV")
        if file_name:
            if not file_name.endswith(".csv"):
                file_name += ".csv"
            with show_error_dialog(self, "Error exporting file"):
                self.write_csv(ws_file, file_name)
                self.statusBar().showMessage(
                    f"Export '{file_name}' finished.", STATUSBAR_MESSAGE_TIMEOUT)

    def handle_set_lang_action(self, lang):
        FilePluginRegistry.lang = lang
        for act in self.lang_actions.values():
            act.setChecked(False)
        self.lang_actions[lang].setChecked(True)

    def write_csv(self, ws_file, file_name):
        with open(file_name, "w") as fp:
            csv_writer = csv.writer(
                fp, delimiter=",", doublequote=False, escapechar='\\',
                lineterminator="\n")
            cls = type(ws_file.data)
            fields = cls.EntryFactory.fields()
            csv_writer.writerow(fields)
            for entry in ws_file.data.entries:
                csv_writer.writerow(entry.values())

    def save_base_content_file(self, ws_file):
        result = QMessageBox.question(
            self, "Save base content file?",
            "Do you really want to update this chunk file?",
            QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Cancel)
        if result == QMessageBox.Ok:
            with show_error_dialog(self, "Error writing file"):
                self.save_workspace_file(ws_file)

    def transfer_file_to_mod_workspace(self, ws_file, reopen=False):
        mod_abs_path, exists = self.mod_directory.get_child_path(ws_file.rel_path)
        if not exists:
            return self.transfer_file(ws_file, self.mod_directory, reopen)

        result = QMessageBox.question(
            self,
            "File exists, overwrite?",
            f"File '{ws_file.rel_path}' already found in mod directory, overwrite?",
            QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok)
        if result == QMessageBox.Ok:
            self.transfer_file(ws_file, self.mod_directory, reopen)

    def transfer_file(self, ws_file, target_directory, reopen=False):
        if target_directory is ws_file.directory:
            return
        self.workspace.close_file(ws_file)
        ws_file.set_directory(target_directory)
        self.save_workspace_file(ws_file)
        if reopen:
            self.workspace.open_file(target_directory, ws_file.abs_path)

    def save_workspace_file(self, ws_file):
        ws_file.save()
        self.statusBar().showMessage(
            f"File '{ws_file.abs_path}' saved.", STATUSBAR_MESSAGE_TIMEOUT)

    def handle_about_action(self):
        dialog = QDialog(self)
        dialog.setWindowTitle("About MHW Editor Suite")
        layout = QVBoxLayout()
        dialog.setLayout(layout)
        about_text = QLabel(ABOUT_TEXT)
        about_text.setTextFormat(Qt.RichText)
        about_text.setTextInteractionFlags(Qt.TextBrowserInteraction)
        about_text.setOpenExternalLinks(True)
        layout.addWidget(about_text)
        dialog.exec()