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)
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()
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()
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
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()