From 0263afec0f9b3891a1ff24447fcee56b58ec122d Mon Sep 17 00:00:00 2001 From: Frederik Schumacher Date: Fri, 3 May 2019 17:06:43 +0200 Subject: [PATCH] import/export full file, individual items. quick access menu --- src/mhw_armor_edit/editor/armor_editor.py | 17 +- src/mhw_armor_edit/editor/itm_editor.py | 7 + src/mhw_armor_edit/editor/models.py | 1 + src/mhw_armor_edit/editor/weapon_editor.py | 13 +- .../editor/weapon_gun_editor.py | 18 ++ src/mhw_armor_edit/import_export.py | 228 ++++++++++-------- src/mhw_armor_edit/models.py | 6 + src/mhw_armor_edit/suite.py | 221 ++++++++++++----- 8 files changed, 338 insertions(+), 173 deletions(-) diff --git a/src/mhw_armor_edit/editor/armor_editor.py b/src/mhw_armor_edit/editor/armor_editor.py index f0456e5..28158f4 100644 --- a/src/mhw_armor_edit/editor/armor_editor.py +++ b/src/mhw_armor_edit/editor/armor_editor.py @@ -5,16 +5,15 @@ from PyQt5 import uic from PyQt5.QtCore import (Qt, QModelIndex) -from PyQt5.QtWidgets import (QDataWidgetMapper, QHeaderView, QMenu) +from PyQt5.QtWidgets import (QDataWidgetMapper, QHeaderView) from mhw_armor_edit.assets import Assets from mhw_armor_edit.editor.models import (SkillTranslationModel, EditorPlugin) from mhw_armor_edit.ftypes.am_dat import AmDatEntry, AmDat -from mhw_armor_edit.import_export import (ExportDialog, ImportDialog, - ImportExportManager) +from mhw_armor_edit.import_export import (ImportExportManager) from mhw_armor_edit.tree import TreeModel, TreeNode -from mhw_armor_edit.utils import ItemDelegate, get_t9n, create_action +from mhw_armor_edit.utils import ItemDelegate, get_t9n log = logging.getLogger() ArmorEditorWidget, ArmorEditorWidgetBase = uic.loadUiType( @@ -243,3 +242,13 @@ class AmDatPlugin(EditorPlugin): "t9n_skill_pt": r"common\text\vfont\skill_pt_eng.gmd", } } + import_export = { + "safe_attrs": [ + "defense", "rarity", "cost", "fire_res", "water_res", "ice_res", + "thunder_res", "dragon_res", "num_gem_slots", "gem_slot1_lvl", + "gem_slot2_lvl", "gem_slot3_lvl", "set_skill1", "set_skill1_lvl", + "set_skill2", "set_skill2_lvl", "skill1", "skill1_lvl", "skill2", + "skill2_lvl", "skill3", "skill3_lvl", "mdl_main_id", + "mdl_secondary_id" + ] + } diff --git a/src/mhw_armor_edit/editor/itm_editor.py b/src/mhw_armor_edit/editor/itm_editor.py index ada5dcb..2cbe8cc 100644 --- a/src/mhw_armor_edit/editor/itm_editor.py +++ b/src/mhw_armor_edit/editor/itm_editor.py @@ -10,6 +10,7 @@ from mhw_armor_edit.assets import Assets from mhw_armor_edit.editor.models import EditorPlugin from mhw_armor_edit.ftypes.itm import Itm +from mhw_armor_edit.import_export import ImportExportManager from mhw_armor_edit.utils import get_t9n_item, get_t9n @@ -120,6 +121,9 @@ def data(self, qindex: QModelIndex, role=None): column = qindex.column() adapt = ModelAdapter(self.model, entry) return adapt[column] + elif role == Qt.UserRole: + entry = self.entries[qindex.row()] + return entry def setData(self, qindex: QModelIndex, value, role=None): if role == Qt.EditRole or role == Qt.DisplayRole: @@ -160,6 +164,9 @@ def __init__(self, parent=None): self.mapper.setModel(self.itm_model) self.item_browser.setModel(self.itm_model) self.item_browser.activated.connect(self.handle_item_browser_activated) + self.import_export_manager = ImportExportManager( + self.item_browser, ItmPlugin.import_export.get("safe_attrs")) + self.import_export_manager.connect_custom_context_menu() self.mapper.addMapping(self.name_value, Column.name, b"text") self.mapper.addMapping(self.id_value, Column.id, b"text") self.mapper.addMapping(self.description_value, Column.description, b"text") diff --git a/src/mhw_armor_edit/editor/models.py b/src/mhw_armor_edit/editor/models.py index 1c978c4..7f65559 100644 --- a/src/mhw_armor_edit/editor/models.py +++ b/src/mhw_armor_edit/editor/models.py @@ -142,6 +142,7 @@ class EditorPlugin: data_factory = None widget_factory = None relations = {} + import_export = {} def __init_subclass__(subcls, **kwargs): super().__init_subclass__(**kwargs) diff --git a/src/mhw_armor_edit/editor/weapon_editor.py b/src/mhw_armor_edit/editor/weapon_editor.py index b49b9a3..c149084 100644 --- a/src/mhw_armor_edit/editor/weapon_editor.py +++ b/src/mhw_armor_edit/editor/weapon_editor.py @@ -89,7 +89,8 @@ def __init__(self, parent=None): self.mapper.setItemDelegate(ItemDelegate()) self.mapper.setModel(self.table_model) self.skill_id_value.setModel(self.skill_model) - self.import_export_manager = ImportExportManager(self.weapon_tree_view) + self.import_export_manager = ImportExportManager( + self.weapon_tree_view, WpDatPlugin.import_export.get("safe_attrs")) self.import_export_manager.connect_custom_context_menu() mappings = [ (self.id_value, WpDatEntry.id.index, b"text"), @@ -262,3 +263,13 @@ class WpDatPlugin(EditorPlugin): } }, } + import_export = { + "safe_attrs": [ + "base_model_id", "part1_id", "part2_id", "color", "is_fixed_upgrade", + "crafting_cost", "rarity", "kire_id", "handicraft", "raw_damage", + "defense", "affinity", "element_id", "element_damage", + "hidden_element_id", "hidden_element_damage", "elderseal", + "num_gem_slots", "gem_slot1_lvl", "gem_slot2_lvl", "gem_slot3_lvl", + "wep1_id", "wep2_id", "skill_id" + ] + } diff --git a/src/mhw_armor_edit/editor/weapon_gun_editor.py b/src/mhw_armor_edit/editor/weapon_gun_editor.py index 7d6f2be..77de5f0 100644 --- a/src/mhw_armor_edit/editor/weapon_gun_editor.py +++ b/src/mhw_armor_edit/editor/weapon_gun_editor.py @@ -12,6 +12,7 @@ from mhw_armor_edit.editor.shell_table_editor import ShellTableTreeModel from mhw_armor_edit.ftypes.bbtbl import BbtblEntry from mhw_armor_edit.ftypes.wp_dat_g import WpDatGEntry, WpDatG +from mhw_armor_edit.import_export import ImportExportManager from mhw_armor_edit.struct_table import StructTableModel from mhw_armor_edit.utils import get_t9n, ItemDelegate @@ -34,6 +35,9 @@ def get_field_value(self, entry, field): def data(self, qindex, role=None): if role == Qt.DisplayRole or role == Qt.EditRole: return super().data(qindex, role) + elif role == Qt.UserRole: + entry = self.entries[qindex.row()] + return entry return None def headerData(self, section, orient, role=None): @@ -92,6 +96,9 @@ def __init__(self, parent=None): self.weapon_tree_view.activated.connect(self.handle_weapon_tree_view_activated) self.skill_model = SkillTranslationModel() self.skill_id_value.setModel(self.skill_model) + self.import_export_manager = ImportExportManager( + self.weapon_tree_view, WpDatGPlugin.import_export.get("safe_attrs")) + self.import_export_manager.connect_custom_context_menu() mappings = [ (self.id_value, WpDatGEntry.id.index, b"text"), (self.name_value, WpDatGEntry.gmd_name_index.index, b"text"), @@ -225,3 +232,14 @@ class WpDatGPlugin(EditorPlugin): } }, } + import_export = { + "safe_attrs": [ + "base_model_id", "part1_id", "part2_id", "color", "is_fixed_upgrade", + "muzzle_type", "barrel_type", "magazine_type", "scope_type", + "crafting_cost", "rarity", "raw_damage", "defense", "affinity", + "element_id", "element_damage", "hidden_element_id", + "hidden_element_damage", "elderseal", "shell_table_id", "deviation", + "num_gem_slots", "gem_slot1_lvl", "gem_slot2_lvl", "gem_slot3_lvl", + "special_ammo_type", "skill_id", "unk6", + ] + } diff --git a/src/mhw_armor_edit/import_export.py b/src/mhw_armor_edit/import_export.py index 85727bc..3cd7299 100644 --- a/src/mhw_armor_edit/import_export.py +++ b/src/mhw_armor_edit/import_export.py @@ -1,28 +1,94 @@ # coding: utf-8 +import csv import json import logging -import sys from PyQt5 import uic -from PyQt5.QtCore import Qt, pyqtSignal, QModelIndex, QObject -from PyQt5.QtWidgets import (QFileDialog, QApplication, QMainWindow, - QPushButton, QWidget, QVBoxLayout, QListWidgetItem, +from PyQt5.QtCore import Qt, pyqtSignal, QModelIndex, QObject, pyqtSlot +from PyQt5.QtWidgets import (QFileDialog, QListWidgetItem, QDialog, QMenu) from mhw_armor_edit.assets import Assets -from mhw_armor_edit.ftypes.am_dat import AmDatEntry -from mhw_armor_edit.utils import create_action +from mhw_armor_edit.utils import create_action, is_sequence, yield_to_list log = logging.getLogger() DialogWidget, DialogWidgetBase = \ uic.loadUiType(Assets.load_asset_file("import_export.ui")) +def sanitize(item, attrs): + return { + attr: item[attr] + for attr in attrs + if attr in item + } + + +class CsvFilter: + Label = "CSV *.csv" + + def import_data(self, fp, fields, as_list=False): + reader = csv.DictReader(fp, fields) + if as_list: + return list(reader) + # just the first one + for row in reader: + return row + + def export_data(self, fp, export_data, fields): + if is_sequence(export_data): + export_data = [sanitize(it, fields) for it in export_data] + else: + export_data = [sanitize(export_data, fields)] + writer = csv.DictWriter(fp, fields, extrasaction="ignore") + writer.writeheader() + writer.writerows(export_data) + + +class JsonFilter: + Label = "JSON *.json" + + def import_data(self, fp, fields, as_list=False): + return json.load(fp) + + def export_data(self, fp, export_data, fields): + if is_sequence(export_data): + export_data = [sanitize(it, fields) for it in export_data] + else: + export_data = sanitize(export_data, fields) + json.dump(export_data, fp, indent=2) + + +class Filters: + registry = { + JsonFilter.Label: JsonFilter, + CsvFilter.Label: CsvFilter + } + + @classmethod + def list(cls): + return ";;".join(cls.registry.keys()) + + @classmethod + def get(cls, spec): + return cls.registry[spec]() + + @classmethod + def first(cls): + for key in cls.registry: + return key + + class DialogHelper: def get_attrs(self): return self.attrs + def is_default_attr(self, attr): + if self.default_attrs is None: + return True + return attr in self.default_attrs + def dialog_helper_init(self): self.check_all_button.clicked.connect(self.handle_check_all_clicked) self.check_none_button.clicked.connect(self.handle_check_none_clicked) @@ -32,7 +98,7 @@ def dialog_helper_init(self): it.setData(Qt.UserRole, attr) it.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) it.setCheckState( - Qt.Checked if attr in self.default_attrs else Qt.Unchecked) + Qt.Checked if self.is_default_attr(attr) else Qt.Unchecked) self.attr_list.addItem(it) def handle_check_all_clicked(self): @@ -45,6 +111,7 @@ def handle_check_none_clicked(self): it = self.attr_list.item(i) it.setCheckState(Qt.Unchecked) + @yield_to_list def get_checked_attrs(self): for i in range(self.attr_list.count()): it = self.attr_list.item(i) @@ -55,41 +122,40 @@ def get_checked_attrs(self): class ImportDialog(DialogHelper, DialogWidgetBase, DialogWidget): import_accepted = pyqtSignal(object) - def __init__(self, parent, data, attrs, default_attrs): + def __init__(self, parent, data, attrs, default_attrs, as_list=False): super().__init__(parent) self.setupUi(self) self.data = data self.attrs = attrs self.default_attrs = default_attrs + self.as_list = as_list self.dialog_helper_init() - self.dialog_button_box.accepted.connect(self.accept) - self.dialog_button_box.rejected.connect(self.reject) self.finished.connect(self.handle_finished) - def get_attrs(self): - for attr in self.attrs: - if attr in self.data: - yield attr - def handle_finished(self, result): - log.debug("handle_finished") if result == QDialog.Accepted: - import_data = { - attr: self.data[attr] - for attr in self.get_checked_attrs() - } - self.import_accepted.emit(import_data) + checked_attrs = self.get_checked_attrs() + if self.as_list: + data = [sanitize(it, checked_attrs) for it in self.data] + else: + data = sanitize(self.data, checked_attrs) + self.import_accepted.emit(data) @classmethod - def init(cls, parent, attrs, default_attrs): - file_path, _ = QFileDialog.getOpenFileName( + def init(cls, parent, attrs, default_attrs, as_list=False): + file_path, selected_filter = QFileDialog.getOpenFileName( parent, "Import data file", - filter="Data *.json;;All *.*", initialFilter="*.json") + filter=Filters.list(), + initialFilter=Filters.first()) if not file_path: return None + filter = Filters.get(selected_filter) + _attrs = [*attrs] + if default_attrs is not None: + _attrs.extend(it for it in default_attrs if it not in _attrs) with open(file_path, "r", encoding="UTF-8") as fp: - data = json.load(fp) - return cls(parent, data, attrs, default_attrs) + data = filter.import_data(fp, _attrs, as_list) + return cls(parent, data, attrs, default_attrs, as_list) class ExportDialog(DialogHelper, DialogWidgetBase, DialogWidget): @@ -101,26 +167,19 @@ def __init__(self, parent, data, attrs, default_attrs): self.attrs = attrs self.default_attrs = default_attrs self.dialog_helper_init() - self.dialog_button_box.accepted.connect(self.handle_accept_button_clicked) - self.dialog_button_box.rejected.connect(self.reject) - - def handle_accept_button_clicked(self): - log.debug("handle_accept_button_clicked") + self.finished.connect(self.handle_finished) - export_path, _ = QFileDialog.getSaveFileName( + def handle_finished(self, result): + if result != QDialog.Accepted: + return + file_path, selected_filter = QFileDialog.getSaveFileName( self, "Export data file", - filter="Data *.json", initialFilter="*.json") - if not export_path: + filter=Filters.list(), initialFilter=Filters.first()) + if not file_path: return self.reject() - - export_data = { - attr: self.data[attr] - for attr in self.get_checked_attrs() - if attr in self.data - } - with open(export_path, "w", encoding="UTF-8") as fp: - json.dump(export_data, fp, indent=2) - self.accept() + filter = Filters.get(selected_filter) + with open(file_path, "w", encoding="UTF-8") as fp: + filter.export_data(fp, self.data, self.get_checked_attrs()) @classmethod def init(cls, parent, data, attrs, default_attrs): @@ -129,94 +188,59 @@ def init(cls, parent, data, attrs, default_attrs): class ImportExportManager(QObject): import_finished = pyqtSignal(int) + export_finished = pyqtSignal(int) - def __init__(self, target_widget): + def __init__(self, target_widget, default_attrs=None): super().__init__(target_widget) self.target_widget = target_widget + self.default_attrs = default_attrs self.export_action = create_action(None, "Export ...", self.handle_export_action) self.import_action = create_action(None, "Import ...", self.handle_import_action) - self.context_menu = QMenu() - self.context_menu.addAction(self.export_action) - self.context_menu.addAction(self.import_action) self.model_index = QModelIndex() + def populate_menu(self, menu): + menu.addAction(self.export_action) + menu.addAction(self.import_action) + def connect_custom_context_menu(self): self.target_widget.setContextMenuPolicy(Qt.CustomContextMenu) self.target_widget.customContextMenuRequested.connect(self.show_context_menu) def show_context_menu(self, point): self.model_index = self.target_widget.indexAt(point) - self.context_menu.exec(self.target_widget.mapToGlobal(point)) + model_data = self.get_model_data() + if model_data is not None: + context_menu = QMenu() + self.populate_menu(context_menu) + context_menu.exec(self.target_widget.mapToGlobal(point)) def handle_export_action(self): if not self.model_index.isValid(): return - model = self.model_index.model() - entry = model.data(self.model_index, Qt.UserRole) - data = entry.as_dict() + data = self.get_model_data() attrs = list(data.keys()) - dialog = ExportDialog.init(self.target_widget, data, attrs, attrs) + dialog = ExportDialog.init(self.target_widget, data, attrs, + self.default_attrs) + dialog.finished.connect(self.export_finished.emit) dialog.open() + def get_model_data(self): + model = self.model_index.model() + entry = model.data(self.model_index, Qt.UserRole) + if entry: + return entry.as_dict() + def handle_import_action(self): if not self.model_index.isValid(): return model = self.model_index.model() entry = model.data(self.model_index, Qt.UserRole) attrs = entry.fields() - dialog = ImportDialog.init(self.target_widget, attrs, attrs) + dialog = ImportDialog.init(self.target_widget, attrs, + self.default_attrs) if dialog: dialog.import_accepted.connect(entry.update) dialog.finished.connect(self.import_finished.emit) dialog.open() - - -class Foo: - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - - -if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG) - app = QApplication(sys.argv) - window = QMainWindow() - window.setWindowTitle("Dialog test") - - def handle_import_accepted(import_data): - log.debug("handle_import_accepted: %s", import_data) - - def handle_import_clicked(): - dialog = ImportDialog.init(window, - AmDatEntry.fields(), - AmDatEntry.fields()) - dialog.import_accepted.connect(handle_import_accepted) - dialog.show() - - def handle_export_clicked(): - export_data = Foo( - id=1, - num_gem_slots=3, - gem_slot1_lvl=3, - gem_slot2_lvl=2, - gem_slot3_lvl=1) - dialog = ExportDialog.init(window, - export_data, - AmDatEntry.fields(), - AmDatEntry.fields()) - dialog.open() - - import_button = QPushButton("Import data ...") - import_button.clicked.connect(handle_import_clicked) - - export_button = QPushButton("Export data ...") - export_button.clicked.connect(handle_export_clicked) - - box = QWidget() - box.setLayout(QVBoxLayout()) - box.layout().addWidget(import_button) - box.layout().addWidget(export_button) - window.setCentralWidget(box) - window.show() - sys.exit(app.exec_()) diff --git a/src/mhw_armor_edit/models.py b/src/mhw_armor_edit/models.py index 6ae3d90..4812abc 100644 --- a/src/mhw_armor_edit/models.py +++ b/src/mhw_armor_edit/models.py @@ -137,6 +137,12 @@ def open_file(self, directory, abs_path): log.exception("error loading path: %s", abs_path) self.fileLoadError.emit(abs_path, rel_path, str(e)) + def open_file_any_dir(self, rel_path): + for directory in self.directories: + abs_path, exists = directory.get_child_path(rel_path) + if exists: + return self.open_file(directory, abs_path) + def close_file(self, ws_file): try: self.files.pop(ws_file.abs_path) diff --git a/src/mhw_armor_edit/suite.py b/src/mhw_armor_edit/suite.py index 5d8739a..d83f3ca 100644 --- a/src/mhw_armor_edit/suite.py +++ b/src/mhw_armor_edit/suite.py @@ -16,6 +16,7 @@ from mhw_armor_edit.assets import Assets from mhw_armor_edit.editor.models import FilePluginRegistry +from mhw_armor_edit.import_export import ExportDialog, ImportDialog from mhw_armor_edit.models import Workspace, Directory from mhw_armor_edit.utils import create_action @@ -45,6 +46,24 @@ ("ptB", "Portuguese"), ("ara", "Arabic"), ) +QUICK_ACCESS_ITEMS = ( + ("Items", r"common\item\itemData.itm"), + ("Armors", r"common\equip\armor.am_dat"), + ("Great Sword", r"common\equip\l_sword.wp_dat"), + ("Sword & Shield", r"common\equip\sword.wp_dat"), + ("Dual Blades", r"common\equip\w_sword.wp_dat"), + ("Longsword", r"common\equip\tachi.wp_dat"), + ("Hammer", r"common\equip\hammer.wp_dat"), + ("Hunting Horn", r"common\equip\whistle.wp_dat"), + ("Lance", r"common\equip\lance.wp_dat"), + ("Gun Lance", r"common\equip\g_lance.wp_dat"), + ("Switch Axe", r"common\equip\s_axe.wp_dat"), + ("Charge Blade", r"common\equip\c_axe.wp_dat"), + ("Insect Glaive", r"common\equip\rod.wp_dat"), + ("Bow", r"common\equip\bow.wp_dat_g"), + ("Heavy Bowgun", r"common\equip\hbg.wp_dat_g"), + ("Light Bowgun", r"common\equip\lbg.wp_dat_g"), +) @contextmanager @@ -55,6 +74,44 @@ def show_error_dialog(parent, title="Error"): QMessageBox.warning(parent, title, str(e), QMessageBox.Ok, QMessageBox.Ok) +class SettingsGroup: + def __init__(self, inst: QSettings, key): + self.inst = inst + inst.beginGroup(key) + + def __setitem__(self, key, value): + self.inst.setValue(key, value) + + def get(self, key, default): + return self.inst.value(key, default) + + def childKeys(self): + return self.inst.childKeys() + + @classmethod + @contextmanager + def begin(cls, settings, key): + try: + yield cls(settings, key) + finally: + settings.endGroup() + + +class AppSettings: + def __init__(self): + self.handle = QSettings(QSettings.IniFormat, QSettings.UserScope, + "fre-sch.github.com", "MHW-Editor-Suite") + + def main_window(self): + return SettingsGroup.begin(self.handle, "MainWindow") + + def application(self): + return SettingsGroup.begin(self.handle, "Application") + + def import_export(self): + return SettingsGroup.begin(self.handle, "ImportExport") + + class EditorView(QWidget): def __init__(self, workspace_file, child_widget, parent=None): super().__init__(parent) @@ -122,12 +179,10 @@ class MainWindow(QMainWindow): def __init__(self): super().__init__() self.chunk_directory = Directory( - "CHUNK", - QIcon(Assets.get_asset_path("document_a4_locked.png")), + "CHUNK", QIcon(Assets.get_asset_path("document_a4_locked.png")), None) self.mod_directory = Directory( - "MOD", - QIcon(Assets.get_asset_path("document_a4.png")), + "MOD", QIcon(Assets.get_asset_path("document_a4.png")), None) self.workspace = Workspace([self.mod_directory, self.chunk_directory], parent=self) @@ -140,14 +195,10 @@ def __init__(self): 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_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() @@ -155,18 +206,20 @@ 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.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: @@ -177,15 +230,16 @@ def load_settings(self): 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() + 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) @@ -207,11 +261,16 @@ def init_actions(self): self.handle_save_file_action, QKeySequence.Save) self.save_file_action.setDisabled(True) - self.export_csv_action = create_action( + self.export_action = create_action( self.get_icon(QStyle.SP_FileIcon), - "Export file to CSV...", + "Export file ...", self.handle_export_file_action) - self.export_csv_action.setDisabled(True) + 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.about_action = create_action( None, "About", self.handle_about_action) self.lang_actions = { @@ -220,18 +279,34 @@ def init_actions(self): 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): - menubar = self.menuBar() - file_menu = menubar.addMenu("File") + 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_csv_action) + file_menu.insertAction(None, self.export_action) + file_menu.insertAction(None, self.import_action) file_menu.insertAction(None, self.save_file_action) - lang_menu = menubar.addMenu("Language") + + 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 = menubar.addMenu("Help") + + # help menu + help_menu = menu_bar.addMenu("Help") help_menu.insertAction(None, self.about_action) def init_toolbar(self): @@ -277,7 +352,8 @@ def handle_workspace_file_opened(self, path, rel_path): 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) + 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) @@ -286,8 +362,10 @@ def handle_workspace_file_activated(self, path, rel_path): 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) + 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}`", @@ -311,8 +389,7 @@ def handle_open_mod_directory(self): 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 + 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: @@ -325,16 +402,35 @@ def handle_save_file_action(self): 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) + 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 @@ -342,16 +438,9 @@ def handle_set_lang_action(self, lang): 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 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(