From 4daba8bb0416909405323c5ae03502f010f293c2 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Mon, 16 May 2016 00:35:26 +0000 Subject: [PATCH 01/27] Updated license text. --- resources/templates/about.ui | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/templates/about.ui b/resources/templates/about.ui index 40f8758..3198447 100644 --- a/resources/templates/about.ui +++ b/resources/templates/about.ui @@ -87,11 +87,11 @@ 0 - 2 + 1 - Copyright 2016 Daniel Nunes <br><br>Licensed under Apache 2.0 + Copyright 2016 Daniel Nunes Qt::AlignCenter @@ -103,11 +103,11 @@ 0 - 2 + 5 - The program is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + <html><head/><body><p>Licensed under the <a href="http://www.apache.org/licenses/LICENSE-2.0"><span style=" text-decoration: underline; color:#0000ff;">Apache License</span></a>, Version 2.0 (the &quot;License&quot;); you may not use this file except in compliance with the <a href="http://www.apache.org/licenses/LICENSE-2.0"><span style=" text-decoration: underline; color:#0000ff;">License</span></a>. Unless required by applicable law or agreed to in writing, software distributed under the <a href="http://www.apache.org/licenses/LICENSE-2.0"><span style=" text-decoration: underline; color:#0000ff;">License</span></a> is distributed on an &quot;AS IS&quot; BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the <a href="http://www.apache.org/licenses/LICENSE-2.0"><span style=" text-decoration: underline; color:#0000ff;">License</span></a> for the specific language governing permissions and limitations under the <a href="http://www.apache.org/licenses/LICENSE-2.0"><span style=" text-decoration: underline; color:#0000ff;">License</span></a>.</p></body></html> Qt::AutoText @@ -116,7 +116,7 @@ false - Qt::AlignHCenter|Qt::AlignTop + Qt::AlignCenter true From 1a1e00affb4d5d3d31b69822cd56502bdc578841 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Mon, 16 May 2016 00:39:06 +0000 Subject: [PATCH 02/27] Fixed changelog. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9107f76..ee819e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -0.4.0 (2016-05-14) +0.4.1 (2016-05-16) -* Urgent bugfix:Fixed wrong default attributes in file and folder tags. +* **Urgent bugfix:** Fixed wrong default attributes in file and folder tags. * Added wizard framework. ---------------------------------- From cd5770094dd6407303403628a239ebba34bc3e33 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Wed, 18 May 2016 02:41:52 +0000 Subject: [PATCH 03/27] Removed gui module dependency from exceptions module. Reworked Wizard framework. Added Files Wizard. --- fomod/exceptions.py | 24 +- fomod/gui.py | 41 +++- fomod/io.py | 5 +- fomod/nodes.py | 15 +- fomod/wizards.py | 307 +++++++++++-------------- resources/templates/mainframe.ui | 2 +- resources/templates/wizard_files_01.ui | 253 ++++++++++++++++++++ 7 files changed, 451 insertions(+), 196 deletions(-) create mode 100644 resources/templates/wizard_files_01.ui diff --git a/fomod/exceptions.py b/fomod/exceptions.py index 2466b21..0f22dfe 100644 --- a/fomod/exceptions.py +++ b/fomod/exceptions.py @@ -16,7 +16,10 @@ from traceback import print_tb from io import StringIO -from . import __version__ +from os.path import join +from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtGui import QPixmap +from . import __version__, cur_folder def excepthook(exc_type, exc_value, tracebackobj): @@ -26,7 +29,6 @@ def excepthook(exc_type, exc_value, tracebackobj): :param exc_value: exception value :param tracebackobj: traceback object """ - from .gui import generic_errorbox notice = ( "An unhandled exception occurred. Please report the problem" @@ -41,7 +43,13 @@ def excepthook(exc_type, exc_value, tracebackobj): errmsg = 'Error information:\n\nVersion: {}\n{}: {}\n'.format(version_info, str(exc_type), str(exc_value)) sections = [errmsg, tbinfo] msg = '\n'.join(sections) - generic_errorbox("Nobody Panic!", notice, msg) + + errorbox = QMessageBox() + errorbox.setText(notice) + errorbox.setWindowTitle("Nobody Panic!") + errorbox.setDetailedText(msg) + errorbox.setIconPixmap(QPixmap(join(cur_folder, "resources/logos/logo_admin.png"))) + errorbox.exec_() class GenericError(Exception): @@ -52,10 +60,10 @@ def __init__(self): class MissingFileError(GenericError): - def __init__(self, file): + def __init__(self, fname): self.title = "I/O Error" - self.message = "{} is missing.".format(file.capitalize()) - self.file = file + self.message = "{} is missing.".format(fname.capitalize()) + self.file = fname Exception.__init__(self, self.message) @@ -80,7 +88,7 @@ def __init__(self, element): class BaseInstanceException(Exception): - def __init__(self, base): + def __init__(self, base_instance): self.title = "Instance Error" - self.message = "{} is not meant to be instanced. A subclass should be used instead.".format(type(base)) + self.message = "{} is not meant to be instanced. A subclass should be used instead.".format(type(base_instance)) Exception.__init__(self, self.message) diff --git a/fomod/gui.py b/fomod/gui.py index 127c6b2..398f7e9 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -102,6 +102,8 @@ def __init__(self): self.object_tree_view.clicked.connect(self.selected_object_tree) self.object_box_list.activated.connect(self.selected_object_list) + self.wizard_button.clicked.connect(self.run_wizard) + self.fomod_changed = False self.original_title = self.windowTitle() self.package_path = "" @@ -213,7 +215,8 @@ def delete(self): except AttributeError: generic_errorbox("You can't do that...", "You can't delete root objects!") - def help(self): + @staticmethod + def help(): not_implemented() def about(self): @@ -290,12 +293,13 @@ def open_(): def selected_object_tree(self, index): self.current_object = self.tree_model.itemFromIndex(index).xml_node + self.object_tree_view.setCurrentIndex(index) if self.settings_dict["General"]["code_refresh"] >= 2: self.update_gen_code() self.update_box_list() self.update_props_list() - self.update_wizards() + self.update_wizard_button() def update_box_list(self): self.list_model.clear() @@ -454,13 +458,40 @@ def update_button_colour(text): prop_list[prop_index].setObjectName(str(prop_index)) prop_index += 1 - def update_wizards(self): - if self.current_object.wizards: + def update_wizard_button(self): + if self.current_object.wizard: self.wizard_button.show() - # do something here, build the wizard gui else: self.wizard_button.hide() + def run_wizard(self): + def close(): + wizard.deleteLater() + self.action_Object_Tree.toggled.emit(enabled_tree) + self.actionObject_Box.toggled.emit(enabled_box) + self.action_Property_Editor.toggled.emit(enabled_list) + self.action_Object_Tree.setEnabled(True) + self.actionObject_Box.setEnabled(True) + self.action_Property_Editor.setEnabled(True) + + current_index = self.tree_model.indexFromItem(self.current_object.model_item) + enabled_tree = self.action_Object_Tree.isChecked() + enabled_box = self.actionObject_Box.isChecked() + enabled_list = self.action_Property_Editor.isChecked() + self.action_Object_Tree.toggled.emit(False) + self.actionObject_Box.toggled.emit(False) + self.action_Property_Editor.toggled.emit(False) + self.action_Object_Tree.setEnabled(False) + self.actionObject_Box.setEnabled(False) + self.action_Property_Editor.setEnabled(False) + + wizard = self.current_object.wizard(self, self.current_object, self) + self.central_widget_layout.insertWidget(0, wizard) + + wizard.cancelled.connect(close) + wizard.finished.connect(close) + wizard.finished.connect(lambda: self.selected_object_tree(current_index)) + def selected_object_list(self, index): item = self.list_model.itemFromIndex(index) diff --git a/fomod/io.py b/fomod/io.py index f9d1770..7a87f40 100644 --- a/fomod/io.py +++ b/fomod/io.py @@ -22,12 +22,13 @@ from pygments import highlight from pygments.formatters.html import HtmlFormatter from pygments.lexers.html import XmlLexer -from . import nodes from .exceptions import MissingFileError, ParserError, TagNotFound class _NodeLookup(PythonElementClassLookup): def lookup(self, doc, element): + from . import nodes + if element.tag == "fomod": return nodes.NodeInfoRoot elif element.tag == "Name": @@ -200,6 +201,8 @@ def import_(package_path): def new(): + from . import nodes + info_root = module_parser.makeelement(nodes.NodeInfoRoot.tag) config_root = module_parser.makeelement(nodes.NodeConfigRoot.tag) diff --git a/fomod/nodes.py b/fomod/nodes.py index 27ad450..cbf1ebf 100644 --- a/fomod/nodes.py +++ b/fomod/nodes.py @@ -17,8 +17,9 @@ from os import sep from PyQt5.QtGui import QStandardItem from lxml import etree -from .exceptions import BaseInstanceException +from .wizards import WizardFiles from .props import PropertyCombo, PropertyInt, PropertyText, PropertyFile, PropertyFolder, PropertyColour +from .exceptions import BaseInstanceException class _NodeBase(etree.ElementBase): @@ -28,14 +29,12 @@ def _init(self): super()._init() def init(self, name, tag, allowed_instances, sort_order=0, allow_text=False, allowed_children=None, properties=None, - wizards=None): + wizard=None): if not properties: properties = {} if not allowed_children: allowed_children = () - if not wizards: - wizards = [] self.name = name self.tag = tag @@ -44,7 +43,7 @@ def init(self, name, tag, allowed_instances, sort_order=0, allow_text=False, all self.allowed_children = allowed_children self.allow_text = allow_text self.allowed_instances = allowed_instances - self.wizards = wizards + self.wizard = wizard self.model_item = NodeStandardItem(self) self.model_item.setText(self.name) @@ -66,6 +65,7 @@ def add_child(self, child): if self.can_add_child(child): self.append(child) self.model_item.appendRow(child.model_item) + child.write_attribs() def remove_child(self, child): if child in self: @@ -235,7 +235,8 @@ class NodeConfigReqFiles(_NodeBase): def _init(self): allowed_children = (NodeConfigFile, NodeConfigFolder) - self.init("Mod Requirements", type(self).tag, 0, allowed_children=allowed_children, sort_order=4) + self.init("Mod Requirements", type(self).tag, 1, allowed_children=allowed_children, + sort_order=4, wizard=WizardFiles) super()._init() @@ -336,7 +337,7 @@ class NodeConfigFiles(_NodeBase): def _init(self): allowed_children = (NodeConfigFile, NodeConfigFolder) - self.init("Files", type(self).tag, 1, allowed_children=allowed_children, sort_order=3) + self.init("Files", type(self).tag, 1, allowed_children=allowed_children, sort_order=3, wizard=WizardFiles) super()._init() diff --git a/fomod/wizards.py b/fomod/wizards.py index 2a9e52f..2cafdf2 100644 --- a/fomod/wizards.py +++ b/fomod/wizards.py @@ -15,198 +15,157 @@ # limitations under the License. from abc import ABCMeta, abstractmethod -from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QStackedWidget +from copy import deepcopy +from os.path import join, relpath +from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QSizePolicy, QLayout, + QStackedWidget, QLineEdit, QLabel, QFormLayout, QFileDialog) +from PyQt5.QtGui import QIcon from PyQt5.QtCore import pyqtSignal +from PyQt5.uic import loadUi +from . import cur_folder +from .io import elem_factory from .exceptions import BaseInstanceException -class _PageBase(QWidget): - __metaclass__ = ABCMeta - - def __init__(self, wizard): - super().__init__(wizard) - if type(self) is _PageBase: - raise BaseInstanceException(self) - - @abstractmethod - def __create_buttons(self): - pass - - -class PageSimple(_PageBase): - def __init__(self, wizard): - super().__init__(wizard) - - def __create_buttons(self): - return - - -class _PageComplex(_PageBase): - def __init__(self, wizard): - super().__init__(wizard) - if type(self) is _PageComplex: - raise BaseInstanceException(self) - - self.main_widget = QWidget(self) - self.button_set = self.__create_buttons() - self.main_layout = QVBoxLayout(self) - self.main_layout.setContentsMargins(5, 5, 5, 5) - self.main_layout.addWidget(self.main_widget) - self.main_layout.addWidget(self.button_set) - - @abstractmethod - def __create_buttons(self): - pass - - -class PageInitial(_PageComplex): - def __init__(self, wizard): - super().__init__(wizard) - self.next_clicked = pyqtSignal() - self.cancel_clicked = pyqtSignal() - - def __create_buttons(self): - next_button = QPushButton(self) - next_button.setText("Next") - next_button.clicked.connect(self.next_clicked.emit()) - cancel_button = QPushButton(self) - cancel_button.setText("Cancel") - cancel_button.clicked.connect(self.cancel_clicked.emit()) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(next_button) - layout.addWidget(cancel_button) - return layout - - -class PageMiddle(_PageComplex): - def __init__(self, wizard): - super().__init__(wizard) - self.next_clicked = pyqtSignal() - self.previous_clicked = pyqtSignal() - self.cancel_clicked = pyqtSignal() - - def __create_buttons(self): - previous_button = QPushButton(self) - previous_button.setText("Previous") - previous_button.clicked.connect(self.previous_clicked.emit()) - next_button = QPushButton(self) - next_button.setText("Next") - next_button.clicked.connect(self.next_clicked.emit()) - cancel_button = QPushButton(self) - cancel_button.setText("Cancel") - cancel_button.clicked.connect(self.cancel_clicked.emit()) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(previous_button) - layout.addWidget(next_button) - layout.addWidget(cancel_button) - return layout - - -class PageFinal(_PageComplex): - def __init__(self, wizard): - super().__init__(wizard) - self.finish_clicked = pyqtSignal() - self.previous_clicked = pyqtSignal() - self.cancel_clicked = pyqtSignal() - - def __create_buttons(self): - previous_button = QPushButton(self) - previous_button.setText("Previous") - previous_button.clicked.connect(self.previous_clicked.emit()) - finish_button = QPushButton(self) - finish_button.setText("Finish") - finish_button.clicked.connect(self.finish_clicked.emit()) - cancel_button = QPushButton(self) - cancel_button.setText("Cancel") - cancel_button.clicked.connect(self.cancel_clicked.emit()) - layout = QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(previous_button) - layout.addWidget(finish_button) - layout.addWidget(cancel_button) - return layout - - class _WizardBase(QStackedWidget): __metaclass__ = ABCMeta - def __init__(self, parent=None): + cancelled = pyqtSignal() + finished = pyqtSignal() + + def __init__(self, parent, element, main_window): super().__init__(parent) if type(self) is _WizardBase: raise BaseInstanceException(self) self.return_data = None - self.pages = self.add_pages() - - @abstractmethod - def process_results(self): - pass - - @abstractmethod - def add_pages(self): - return [] - - -class _WizardSimple(_WizardBase): - def __init__(self, parent=None): - super().__init__(parent) - if type(self) is _WizardSimple: - raise BaseInstanceException(self) - - @abstractmethod - def process_results(self): - pass - - @abstractmethod - def add_pages(self): - return [] - - -class _WizardComplex(_WizardBase): - def __init__(self, parent=None): - super().__init__(parent) - if type(self) is _WizardComplex: - raise BaseInstanceException(self) - - self.initial_page = PageInitial(self) - self.initial_page.next_clicked.connect(self.next_page) - self.initial_page.cancel_clicked.connect(self.cancelled_wizard.emit()) - self.final_page = PageFinal(self) - self.final_page.previous_clicked.connect(self.previous_page) - self.final_page.finish_clicked.connect(self.finished_wizard.emit(self.process_results())) - self.final_page.cancel_clicked.connect(self.cancelled_wizard.emit()) - - self.cancelled_wizard = pyqtSignal() - self.finished_wizard = pyqtSignal() - + self.element = element + self.parent = parent + self.main_window = main_window + self.pages = self._add_pages() for page in self.pages: - page.previous_clicked.connect(self.previous_page) - page.next_clicked.connect(self.next_page) - page.cancel_clicked.connect(self.cancelled_wizard.emit()) - - self.pages.insert(0, self.initial_page) - self.pages.append(self.final_page) + self.addWidget(page) @abstractmethod - def process_results(self): + def _process_results(self, result): pass @abstractmethod - def initial_page(self): - return PageInitial(self) - - @abstractmethod - def add_pages(self): + def _add_pages(self): return [] - @abstractmethod - def final_page(self): - return PageFinal(self) - - def next_page(self): - self.setCurrentIndex(self.pages.index(self.sender()) + 1) - def previous_page(self): - self.setCurrentIndex(self.pages.index(self.sender()) - 1) +class WizardFiles(_WizardBase): + def _process_results(self, result): + self.element.getparent().replace(self.element, result) + item_parent = self.element.model_item.parent() + row = self.element.model_item.row() + item_parent.removeRow(row) + item_parent.insertRow(row, result.model_item) + self.finished.emit() + + def _add_pages(self): + def add_elem(element_, layout): + """ + :param element_: The element to be copied + :param layout: The layout into which to insert the newly copied element + """ + child = elem_factory(element_.tag, element_result) + for key in element_.properties: + child.properties[key].set_value(element_.properties[key].value) + element_result.add_child(child) + spacer = layout.takeAt(layout.count() - 1) + layout.addWidget(self._create_field(child, page)) + layout.addSpacerItem(spacer) + + element_result = deepcopy(self.element) + + page = loadUi(join(cur_folder, "resources/templates/wizard_files_01.ui")) + + file_list = [elem for elem in element_result if elem.tag == "file"] + for element in file_list: + add_elem(element, page.layout_file) + element_result.remove_child(element) + + folder_list = [elem for elem in element_result if elem.tag == "folder"] + for element in folder_list: + add_elem(element, page.layout_folder) + element_result.remove_child(element) + + # finish with connections + page.button_add_file.clicked.connect(lambda: add_elem(elem_factory("file", element_result), page.layout_file)) + page.button_add_folder.clicked.connect( + lambda: add_elem(elem_factory("folder", element_result), page.layout_folder)) + page.finish_button.clicked.connect(lambda: self._process_results(element_result)) + page.cancel_button.clicked.connect(self.cancelled.emit) + + return [page] + + def _create_field(self, element, parent): + """ + :param element: the element newly copied + :param parent: the parent widget (the QWidgets inside the scroll areas) + :return: base QWidget, with the source and destination fields built + """ + def button_clicked(): + open_dialog = QFileDialog() + if element.tag == "file": + file_path = open_dialog.getOpenFileName(self, "Select File:", self.main_window.package_path) + if file_path[0]: + edit_source.setText(relpath(file_path[0], self.main_window.package_path)) + elif element.tag == "folder": + folder_path = open_dialog.getExistingDirectory(self, "Select folder:", self.main_window.package_path) + if folder_path: + edit_source.setText(relpath(folder_path, self.main_window.package_path)) + + # main widget + base = QWidget(parent) + layout_main = QHBoxLayout(base) + layout_main.setContentsMargins(0, 0, 0, 0) + + # the entire source form, label + (edit + button) + layout_source = QHBoxLayout() + layout_source.setContentsMargins(0, 0, 0, 0) + label_source = QLabel("Source:", base) + + # the source box (edit + button) + base_source = QWidget(base) + layout_source_field = QHBoxLayout(base_source) + layout_source_field.setContentsMargins(0, 0, 0, 0) + edit_source = QLineEdit(element.get("source"), base_source) + button_source = QPushButton("...", base_source) + layout_source_field.addWidget(edit_source) + layout_source_field.addWidget(button_source) + + # finish the source form + layout_source.addWidget(label_source) + layout_source.addWidget(base_source) + + # the entire destination form, label + (edit + button) + layout_dest = QHBoxLayout() + layout_dest.setContentsMargins(0, 0, 0, 0) + label_dest = QLabel("Destination:", base) + edit_dest = QLineEdit(element.get("destination"), base) + layout_dest.addWidget(label_dest) + layout_dest.addWidget(edit_dest) + + # the delete self button + button_delete = QPushButton(QIcon(join(cur_folder, "resources/logos/logo_cross.png")), "", base) + button_delete.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + button_delete.setMaximumSize(30, 30) + + # finish the main widget + layout_main.addLayout(layout_source) + layout_main.addLayout(layout_dest) + layout_main.addWidget(button_delete) + + # connect the signals + edit_source.textChanged.connect(element.properties["source"].set_value) + edit_source.textChanged.connect(element.write_attribs) + edit_dest.textChanged.connect(element.properties["destination"].set_value) + edit_dest.textChanged.connect(element.write_attribs) + button_source.clicked.connect(button_clicked) + button_delete.clicked.connect(base.deleteLater) + button_delete.clicked.connect(lambda x: element.getparent().remove_child(element)) + + return base diff --git a/resources/templates/mainframe.ui b/resources/templates/mainframe.ui index 883edff..41b2df8 100644 --- a/resources/templates/mainframe.ui +++ b/resources/templates/mainframe.ui @@ -14,7 +14,7 @@ FOMOD Designer - + 6 diff --git a/resources/templates/wizard_files_01.ui b/resources/templates/wizard_files_01.ui new file mode 100644 index 0000000..141c656 --- /dev/null +++ b/resources/templates/wizard_files_01.ui @@ -0,0 +1,253 @@ + + + Form + + + + 0 + 0 + 470 + 539 + + + + Form + + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + 14 + 75 + true + + + + Files and Folders Wizard + + + Qt::AlignCenter + + + + + + + TODO + + + + + + + + 75 + true + true + + + + Files To Be Installed: + + + Qt::AlignCenter + + + + + + + Add File + + + + + + + true + + + + + 0 + 0 + 448 + 159 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 75 + true + true + + + + Folders To Be Installed: + + + Qt::AlignCenter + + + + + + + Add Folder + + + + + + + true + + + + + 0 + 0 + 448 + 158 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Finish + + + + + + + + 0 + 0 + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + From 780e16a2c59b8d1febdcad89010d5d1698ba187f Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 19 May 2016 21:01:45 +0000 Subject: [PATCH 04/27] Using a wizard should now disable all menus except help. Calls to updating the generated xml code are now routed through a signal. Added splitter between previews and wizard. Fixed source button width in files wizard. Fixed attribute setting for wizards. --- fomod/gui.py | 99 +++++++++----------- fomod/io.py | 2 + fomod/wizards.py | 25 ++++-- resources/templates/mainframe.ui | 120 +++++++++++++------------ resources/templates/wizard_files_01.ui | 35 ++++++-- 5 files changed, 151 insertions(+), 130 deletions(-) diff --git a/fomod/gui.py b/fomod/gui.py index 398f7e9..ae158d5 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -22,7 +22,7 @@ from PyQt5.QtWidgets import (QShortcut, QFileDialog, QColorDialog, QMessageBox, QLabel, QHBoxLayout, QFormLayout, QLineEdit, QSpinBox, QComboBox, QWidget, QPushButton) from PyQt5.QtGui import QIcon, QPixmap, QStandardItemModel, QColor -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, pyqtSignal from validator import validate_tree, check_warnings, ValidatorError, ValidationError, WarningError from . import cur_folder, __version__ from .io import import_, new, export, sort_elements, elem_factory, highlight_fragment @@ -36,50 +36,21 @@ class MainFrame(base_ui[0], base_ui[1]): + xml_code_changed = pyqtSignal([object]) + def __init__(self): super().__init__() self.setupUi(self) # setup the icons properly - window_icon = QIcon() - window_icon.addPixmap(QPixmap(join(cur_folder, "resources/window_icon.jpg")), - QIcon.Normal, QIcon.Off) - self.setWindowIcon(window_icon) - - icon_open = QIcon() - icon_open.addPixmap(QPixmap(join(cur_folder, "resources/logos/logo_open_file.png")), - QIcon.Normal, QIcon.Off) - self.action_Open.setIcon(icon_open) - - icon_save = QIcon() - icon_save.addPixmap(QPixmap(join(cur_folder, "resources/logos/logo_floppy_disk.png")), - QIcon.Normal, QIcon.Off) - self.action_Save.setIcon(icon_save) - - icon_options = QIcon() - icon_options.addPixmap(QPixmap(join(cur_folder, "resources/logos/logo_gear.png")), - QIcon.Normal, QIcon.Off) - self.actionO_ptions.setIcon(icon_options) - - icon_refresh = QIcon() - icon_refresh.addPixmap(QPixmap(join(cur_folder, "resources/logos/logo_refresh.png")), - QIcon.Normal, QIcon.Off) - self.action_Refresh.setIcon(icon_refresh) - - icon_delete = QIcon() - icon_delete.addPixmap(QPixmap(join(cur_folder, "resources/logos/logo_cross.png")), - QIcon.Normal, QIcon.Off) - self.action_Delete.setIcon(icon_delete) - - icon_about = QIcon() - icon_about.addPixmap(QPixmap(join(cur_folder, "resources/logos/logo_notepad.png")), - QIcon.Normal, QIcon.Off) - self.action_About.setIcon(icon_about) - - icon_help = QIcon() - icon_help.addPixmap(QPixmap(join(cur_folder, "resources/logos/logo_info.png")), - QIcon.Normal, QIcon.Off) - self.actionHe_lp.setIcon(icon_help) + self.setWindowIcon(QIcon(join(cur_folder, "resources/window_icon.jpg"))) + self.action_Open.setIcon(QIcon(join(cur_folder, "resources/logos/logo_open_file.png"))) + self.action_Save.setIcon(QIcon(join(cur_folder, "resources/logos/logo_floppy_disk.png"))) + self.actionO_ptions.setIcon(QIcon(join(cur_folder, "resources/logos/logo_gear.png"))) + self.action_Refresh.setIcon(QIcon(join(cur_folder, "resources/logos/logo_refresh.png"))) + self.action_Delete.setIcon(QIcon(join(cur_folder, "resources/logos/logo_cross.png"))) + self.action_About.setIcon(QIcon(join(cur_folder, "resources/logos/logo_notepad.png"))) + self.actionHe_lp.setIcon(QIcon(join(cur_folder, "resources/logos/logo_info.png"))) # setup any additional info left from designer self.delete_sec_shortcut = QShortcut(self) @@ -90,7 +61,6 @@ def __init__(self): self.actionO_ptions.triggered.connect(self.options) self.action_Refresh.triggered.connect(self.refresh) self.action_Delete.triggered.connect(self.delete) - # noinspection PyUnresolvedReferences self.delete_sec_shortcut.activated.connect(self.delete) self.actionHe_lp.triggered.connect(self.help) self.action_About.triggered.connect(self.about) @@ -103,6 +73,7 @@ def __init__(self): self.object_box_list.activated.connect(self.selected_object_list) self.wizard_button.clicked.connect(self.run_wizard) + self.xml_code_changed.connect(self.update_gen_code) self.fomod_changed = False self.original_title = self.windowTitle() @@ -164,6 +135,7 @@ def open(self, path=""): self.package_name = basename(normpath(self.package_path)) self.fomod_modified(False) self.current_object = None + self.xml_code_changed.emit(self.current_object) self.update_recent_files(self.package_path) except (GenericError, ValidatorError) as p: generic_errorbox(p.title, str(p), p.detailed) @@ -202,7 +174,7 @@ def options(self): def refresh(self): if self.settings_dict["General"]["code_refresh"] >= 1: - self.update_gen_code() + self.xml_code_changed.emit(self.current_object) def delete(self): try: @@ -295,7 +267,7 @@ def selected_object_tree(self, index): self.current_object = self.tree_model.itemFromIndex(index).xml_node self.object_tree_view.setCurrentIndex(index) if self.settings_dict["General"]["code_refresh"] >= 2: - self.update_gen_code() + self.xml_code_changed.emit(self.current_object) self.update_box_list() self.update_props_list() @@ -334,6 +306,9 @@ def update_props_list(self): prop_list[prop_index].textEdited[str].connect(self.current_object.set_text) prop_list[prop_index].textEdited[str].connect(self.current_object.write_attribs) prop_list[prop_index].textEdited[str].connect(self.fomod_modified) + prop_list[prop_index].textEdited[str].connect(lambda: self.xml_code_changed.emit(self.current_object) + if self.settings_dict["General"]["code_refresh"] >= 3 + else None) self.formLayout.setWidget(prop_index, QFormLayout.FieldRole, prop_list[prop_index]) @@ -355,6 +330,9 @@ def update_props_list(self): prop_list[prop_index].textEdited[str].connect(self.current_object.write_attribs) prop_list[prop_index].textEdited[str].connect(self.current_object.update_item_name) prop_list[prop_index].textEdited[str].connect(self.fomod_modified) + prop_list[prop_index].textEdited[str].connect(lambda: self.xml_code_changed.emit(self.current_object) + if self.settings_dict["General"]["code_refresh"] >= 3 + else None) elif isinstance(props[key], PropertyInt): prop_list.append(QSpinBox(self.dockWidgetContents)) @@ -364,6 +342,9 @@ def update_props_list(self): prop_list[prop_index].valueChanged.connect(props[key].set_value) prop_list[prop_index].valueChanged.connect(self.current_object.write_attribs) prop_list[prop_index].valueChanged.connect(self.fomod_modified) + prop_list[prop_index].valueChanged.connect(lambda: self.xml_code_changed.emit(self.current_object) + if self.settings_dict["General"]["code_refresh"] >= 3 + else None) elif isinstance(props[key], PropertyCombo): prop_list.append(QComboBox(self.dockWidgetContents)) @@ -373,6 +354,9 @@ def update_props_list(self): prop_list[prop_index].activated[str].connect(self.current_object.write_attribs) prop_list[prop_index].activated[str].connect(self.current_object.update_item_name) prop_list[prop_index].activated[str].connect(self.fomod_modified) + prop_list[prop_index].activated[str].connect(lambda: self.xml_code_changed.emit(self.current_object) + if self.settings_dict["General"]["code_refresh"] >= 3 + else None) elif isinstance(props[key], PropertyFile): def button_clicked(): @@ -394,6 +378,8 @@ def button_clicked(): line_edit.textChanged[str].connect(self.current_object.write_attribs) line_edit.textChanged[str].connect(self.current_object.update_item_name) line_edit.textChanged[str].connect(self.fomod_modified) + line_edit.textChanged[str].connect(lambda: self.xml_code_changed.emit(self.current_object) + if self.settings_dict["General"]["code_refresh"] >= 3 else None) path_button.clicked.connect(button_clicked) elif isinstance(props[key], PropertyFolder): @@ -416,6 +402,8 @@ def button_clicked(): line_edit.textChanged.connect(self.current_object.write_attribs) line_edit.textChanged.connect(self.current_object.update_item_name) line_edit.textChanged.connect(self.fomod_modified) + line_edit.textChanged.connect(lambda: self.xml_code_changed.emit(self.current_object) + if self.settings_dict["General"]["code_refresh"] >= 3 else None) path_button.clicked.connect(button_clicked) elif isinstance(props[key], PropertyColour): @@ -452,6 +440,8 @@ def update_button_colour(text): line_edit.textChanged.connect(update_button_colour) line_edit.textChanged.connect(self.current_object.write_attribs) line_edit.textChanged.connect(self.fomod_modified) + line_edit.textChanged.connect(lambda: self.xml_code_changed.emit(self.current_object) + if self.settings_dict["General"]["code_refresh"] >= 3 else None) path_button.clicked.connect(button_clicked) self.formLayout.setWidget(prop_index, QFormLayout.FieldRole, prop_list[prop_index]) @@ -470,9 +460,9 @@ def close(): self.action_Object_Tree.toggled.emit(enabled_tree) self.actionObject_Box.toggled.emit(enabled_box) self.action_Property_Editor.toggled.emit(enabled_list) - self.action_Object_Tree.setEnabled(True) - self.actionObject_Box.setEnabled(True) - self.action_Property_Editor.setEnabled(True) + self.menu_File.setEnabled(True) + self.menu_Tools.setEnabled(True) + self.menu_View.setEnabled(True) current_index = self.tree_model.indexFromItem(self.current_object.model_item) enabled_tree = self.action_Object_Tree.isChecked() @@ -481,16 +471,17 @@ def close(): self.action_Object_Tree.toggled.emit(False) self.actionObject_Box.toggled.emit(False) self.action_Property_Editor.toggled.emit(False) - self.action_Object_Tree.setEnabled(False) - self.actionObject_Box.setEnabled(False) - self.action_Property_Editor.setEnabled(False) + self.menu_File.setEnabled(False) + self.menu_Tools.setEnabled(False) + self.menu_View.setEnabled(False) wizard = self.current_object.wizard(self, self.current_object, self) - self.central_widget_layout.insertWidget(0, wizard) + self.splitter.insertWidget(0, wizard) wizard.cancelled.connect(close) wizard.finished.connect(close) wizard.finished.connect(lambda: self.selected_object_tree(current_index)) + wizard.finished.connect(lambda: self.fomod_modified(True)) def selected_object_list(self, index): item = self.list_model.itemFromIndex(index) @@ -512,15 +503,13 @@ def selected_object_list(self, index): # set the installer as changed self.fomod_modified(True) - def update_gen_code(self): - if self.current_object is not None: - self.xml_code_browser.setHtml(highlight_fragment(self.current_object)) + def update_gen_code(self, element): + if element is not None: + self.xml_code_browser.setHtml(highlight_fragment(element)) else: self.xml_code_browser.setText("") def fomod_modified(self, changed): - if self.settings_dict["General"]["code_refresh"] >= 3: - self.update_gen_code() if changed is False: self.fomod_changed = False self.setWindowTitle(self.package_name + " - " + self.original_title) diff --git a/fomod/io.py b/fomod/io.py index 7a87f40..2d5e9b0 100644 --- a/fomod/io.py +++ b/fomod/io.py @@ -192,6 +192,8 @@ def import_(package_path): if not _validate_child(elem): element.remove_child(elem) + element.write_attribs() + except ParseError as e: raise ParserError(str(e)) except MissingFileError: diff --git a/fomod/wizards.py b/fomod/wizards.py index 2cafdf2..08cbb9a 100644 --- a/fomod/wizards.py +++ b/fomod/wizards.py @@ -17,8 +17,8 @@ from abc import ABCMeta, abstractmethod from copy import deepcopy from os.path import join, relpath -from PyQt5.QtWidgets import (QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QSizePolicy, QLayout, - QStackedWidget, QLineEdit, QLabel, QFormLayout, QFileDialog) +from PyQt5.QtWidgets import (QHBoxLayout, QWidget, QPushButton, QSizePolicy, + QStackedWidget, QLineEdit, QLabel, QFileDialog) from PyQt5.QtGui import QIcon from PyQt5.QtCore import pyqtSignal from PyQt5.uic import loadUi @@ -71,12 +71,13 @@ def add_elem(element_, layout): :param layout: The layout into which to insert the newly copied element """ child = elem_factory(element_.tag, element_result) - for key in element_.properties: - child.properties[key].set_value(element_.properties[key].value) + for key in element_.attrib: + child.properties[key].set_value(element_.attrib[key]) element_result.add_child(child) spacer = layout.takeAt(layout.count() - 1) layout.addWidget(self._create_field(child, page)) layout.addSpacerItem(spacer) + self.main_window.xml_code_changed.emit(element_result) element_result = deepcopy(self.element) @@ -101,10 +102,10 @@ def add_elem(element_, layout): return [page] - def _create_field(self, element, parent): + def _create_field(self, element, parent_widget): """ :param element: the element newly copied - :param parent: the parent widget (the QWidgets inside the scroll areas) + :param parent_widget: the parent widget (the QWidgets inside the scroll areas) :return: base QWidget, with the source and destination fields built """ def button_clicked(): @@ -118,8 +119,10 @@ def button_clicked(): if folder_path: edit_source.setText(relpath(folder_path, self.main_window.package_path)) + parent_element = element.getparent() + # main widget - base = QWidget(parent) + base = QWidget(parent_widget) layout_main = QHBoxLayout(base) layout_main.setContentsMargins(0, 0, 0, 0) @@ -134,6 +137,7 @@ def button_clicked(): layout_source_field.setContentsMargins(0, 0, 0, 0) edit_source = QLineEdit(element.get("source"), base_source) button_source = QPushButton("...", base_source) + button_source.setMaximumSize(50, 30) layout_source_field.addWidget(edit_source) layout_source_field.addWidget(button_source) @@ -152,7 +156,7 @@ def button_clicked(): # the delete self button button_delete = QPushButton(QIcon(join(cur_folder, "resources/logos/logo_cross.png")), "", base) button_delete.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - button_delete.setMaximumSize(30, 30) + button_delete.setMaximumSize(24, 24) # finish the main widget layout_main.addLayout(layout_source) @@ -162,10 +166,13 @@ def button_clicked(): # connect the signals edit_source.textChanged.connect(element.properties["source"].set_value) edit_source.textChanged.connect(element.write_attribs) + edit_source.textChanged.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) edit_dest.textChanged.connect(element.properties["destination"].set_value) edit_dest.textChanged.connect(element.write_attribs) + edit_dest.textChanged.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) button_source.clicked.connect(button_clicked) button_delete.clicked.connect(base.deleteLater) - button_delete.clicked.connect(lambda x: element.getparent().remove_child(element)) + button_delete.clicked.connect(lambda x: parent_element.remove_child(element)) + button_delete.clicked.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) return base diff --git a/resources/templates/mainframe.ui b/resources/templates/mainframe.ui index 41b2df8..e67d8dd 100644 --- a/resources/templates/mainframe.ui +++ b/resources/templates/mainframe.ui @@ -14,10 +14,7 @@ FOMOD Designer - - - 6 - + 0 @@ -31,62 +28,67 @@ 0 - - - 0 + + + Qt::Horizontal - - - Preview - - - - - - <html><head/><body><p>The preview is not ready yet!</p><p>Meanwhile, try to install your mod using NMM or MO to check if everything is ok.</p></body></html> - - - Qt::AlignCenter - - - true - - - - - - - - XML Code - - - - - - 1 - - - Generated XML Code - - - Qt::AlignCenter - - - - - - - Qt::ScrollBarAsNeeded - - - QTextEdit::NoWrap - - - Click a node to see the generated XML code here. - - - - + + + 0 + + + + Preview + + + + + + <html><head/><body><p>The preview is not ready yet!</p><p>Meanwhile, try to install your mod using NMM or MO to check if everything is ok.</p></body></html> + + + Qt::AlignCenter + + + true + + + + + + + + XML Code + + + + + + 1 + + + Generated XML Code + + + Qt::AlignCenter + + + + + + + Qt::ScrollBarAsNeeded + + + QTextEdit::NoWrap + + + Click a node to see the generated XML code here. + + + + + diff --git a/resources/templates/wizard_files_01.ui b/resources/templates/wizard_files_01.ui index 141c656..d79f7aa 100644 --- a/resources/templates/wizard_files_01.ui +++ b/resources/templates/wizard_files_01.ui @@ -48,7 +48,22 @@ - TODO + <html><head/><body><p align="justify">Select the files and folders to be installed. The &quot;<span style=" font-style:italic;">Source&quot;</span> fields are used to select the file/folder to be installed. The <span style=" font-style:italic;">&quot;Destination&quot;</span> fields are used to select where the item will be installed to - a blank fields corresponds to the <span style=" font-style:italic;">Data</span> folder itself.</p><p align="justify">See the <span style=" text-decoration: underline;">Help</span> for more information.</p><p align="justify"><span style=" font-weight:600;">Note:</span> When a folder is selected, it will be recursively installed - all the files and folders under it, and so on.</p></body></html> + + + Qt::AutoText + + + false + + + true + + + 8 + + + -1 @@ -77,7 +92,10 @@ - + + + Qt::ScrollBarAlwaysOn + true @@ -86,8 +104,8 @@ 0 0 - 448 - 159 + 431 + 106 @@ -145,7 +163,10 @@ - + + + Qt::ScrollBarAlwaysOn + true @@ -154,8 +175,8 @@ 0 0 - 448 - 158 + 431 + 106 From eee0679970a06095c2f3e8b56aafdb344af7a5d2 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 19 May 2016 21:50:44 +0000 Subject: [PATCH 05/27] Added save state check when opening packages. Property editor should now be cleared when opening a package. --- fomod/gui.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/fomod/gui.py b/fomod/gui.py index ae158d5..5b14b9e 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -98,6 +98,14 @@ def __init__(self): def open(self, path=""): try: + answer = self.check_fomod_state() + if answer == QMessageBox.Save: + self.save() + elif answer == QMessageBox.Cancel: + return + else: + pass + if not path: open_dialog = QFileDialog() package_path = open_dialog.getExistingDirectory(self, "Select package root directory:", expanduser("~")) @@ -137,6 +145,7 @@ def open(self, path=""): self.current_object = None self.xml_code_changed.emit(self.current_object) self.update_recent_files(self.package_path) + self.clear_prop_list() except (GenericError, ValidatorError) as p: generic_errorbox(p.title, str(p), p.detailed) return @@ -283,14 +292,16 @@ def update_box_list(self): self.list_model.appendRow(new_object.model_item) self.current_children_list.append(new_object) - # noinspection PyUnresolvedReferences - def update_props_list(self): + def clear_prop_list(self): self.current_prop_list.clear() for index in reversed(range(self.formLayout.count())): widget = self.formLayout.takeAt(index).widget() if widget is not None: widget.deleteLater() + def update_props_list(self): + self.clear_prop_list() + prop_index = 0 prop_list = self.current_prop_list props = self.current_object.properties @@ -517,7 +528,7 @@ def fomod_modified(self, changed): self.fomod_changed = True self.setWindowTitle("*" + self.package_name + " - " + self.original_title) - def closeEvent(self, event): + def check_fomod_state(self): if self.fomod_changed: msg_box = QMessageBox() msg_box.setWindowTitle("The installer has been modified.") @@ -526,7 +537,12 @@ def closeEvent(self, event): QMessageBox.Discard | QMessageBox.Cancel) msg_box.setDefaultButton(QMessageBox.Save) - answer = msg_box.exec_() + return msg_box.exec_() + else: + return + + def closeEvent(self, event): + answer = self.check_fomod_state() if answer == QMessageBox.Save: self.save() elif answer == QMessageBox.Discard: From 30f23049c56fa4140b8a8c310cc092b48e063959 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 20 May 2016 00:05:54 +0000 Subject: [PATCH 06/27] Added check to path existence in recent files. --- fomod/gui.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/fomod/gui.py b/fomod/gui.py index 5b14b9e..c018707 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -15,7 +15,7 @@ # limitations under the License. from os import makedirs -from os.path import expanduser, normpath, basename, join, relpath +from os.path import expanduser, normpath, basename, join, relpath, isdir from datetime import datetime from configparser import ConfigParser from PyQt5.uic import loadUiType @@ -238,16 +238,31 @@ def clear_recent_files(self): del child def update_recent_files(self, add_new=None): - def open_path(instance, package): - def open_(): - instance.open(package) - return open_ + def invalid_path(path_): + msg_box = QMessageBox() + msg_box.setWindowTitle("This path no longer exists.") + msg_box.setText("Remove it from the Recent Files list?") + msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msg_box.setDefaultButton(QMessageBox.Yes) + answer = msg_box.exec_() + config_ = ConfigParser() + config_.read_dict(read_settings()) + if answer == QMessageBox.Yes: + for key in config_["Recent Files"]: + if config_["Recent Files"][key] == path_: + config_["Recent Files"][key] = "" + with open(join(expanduser("~"), ".fomod", ".designer"), "w") as configfile_: + config_.write(configfile_) + self.update_recent_files() + elif answer == QMessageBox.No: + pass file_list = [] settings = read_settings() for index in range(1, 5): if settings["Recent Files"]["file" + str(index)]: file_list.append(settings["Recent Files"]["file" + str(index)]) + file_list = sorted(set(file_list)) # remove all duplicates there was an issue with duplicate after invalid path self.clear_recent_files() if add_new: @@ -268,7 +283,7 @@ def open_(): self.menu_Recent_Files.removeAction(self.actionClear) for path in file_list: action = self.menu_Recent_Files.addAction(path) - action.triggered.connect(open_path(self, path)) + action.triggered.connect(lambda x, path_=path: self.open(path_) if isdir(path_) else invalid_path(path_)) self.menu_Recent_Files.addSeparator() self.menu_Recent_Files.addAction(self.actionClear) From 9d7d5455012fdc07f9b41fabf60c4b9b3907b936 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 20 May 2016 00:50:19 +0000 Subject: [PATCH 07/27] Changed repo name. Updated Pyinstaller to version 3.2. --- .python-version | 2 +- appveyor.yml | 4 ++-- dev/appveyor-bootstrap.bat | 6 +++--- dev/reqs.txt | 2 +- dev/travis-bootstrap.sh | 4 ++-- dev/travis-build.sh | 2 +- dev/vagrant-bootstrap.sh | 4 ++-- fomod/exceptions.py | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.python-version b/.python-version index bd682ac..57ef1a3 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -miniconda3-3.19.0/envs/fomod-editor +miniconda3-3.19.0/envs/fomod-designer diff --git a/appveyor.yml b/appveyor.yml index a746a05..fd27717 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,7 +15,7 @@ deploy: auth_token: secure: iMaZrvVT+OI/9jRs8LyOvmzVqIBa0/jpiK96wNzZww/KqKsMcferhIeSK7faNzOo artifact: windows_build - description: '[Changelog.](https://github.com/GandaG/fomod-editor/blob/master/CHANGELOG.md)' + description: '[Changelog.](https://github.com/GandaG/fomod-designer/blob/master/CHANGELOG.md)' force_update: true on: - appveyor_repo_tag: true \ No newline at end of file + appveyor_repo_tag: true diff --git a/dev/appveyor-bootstrap.bat b/dev/appveyor-bootstrap.bat index f1826b8..7855a30 100644 --- a/dev/appveyor-bootstrap.bat +++ b/dev/appveyor-bootstrap.bat @@ -2,12 +2,12 @@ set PATH=C:\Miniconda-x64;C:\Miniconda-x64\Scripts;%PATH% -conda create -y -n fomod-editor^ +conda create -y -n fomod-designer^ -c https://conda.anaconda.org/mmcauliffe^ -c https://conda.anaconda.org/anaconda^ pyqt5=5.5.1 python=3.5.1 lxml=3.5.0 -call activate fomod-editor +call activate fomod-designer pip install pip -U pip install setuptools -U --ignore-installed -pip install -r dev\reqs.txt \ No newline at end of file +pip install -r dev\reqs.txt diff --git a/dev/reqs.txt b/dev/reqs.txt index 17f6c45..72374d2 100644 --- a/dev/reqs.txt +++ b/dev/reqs.txt @@ -3,4 +3,4 @@ fomod-validator==1.2.0 invoke==0.12.2 lxml==3.5.0 Pygments==2.1.3 -PyInstaller==3.1.1 +PyInstaller==3.2 diff --git a/dev/travis-bootstrap.sh b/dev/travis-bootstrap.sh index 2a6e176..112207e 100644 --- a/dev/travis-bootstrap.sh +++ b/dev/travis-bootstrap.sh @@ -39,11 +39,11 @@ env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install miniconda3-3.19.0 # make the virtualenv pyenv shell miniconda3-3.19.0 -conda create -y -n fomod-editor \ +conda create -y -n fomod-designer \ -c https://conda.anaconda.org/mmcauliffe \ -c https://conda.anaconda.org/anaconda \ python=3.5.1 pyqt5=5.5.1 lxml=3.5.0 -pyenv shell miniconda3-3.19.0/envs/fomod-editor +pyenv shell miniconda3-3.19.0/envs/fomod-designer # install the pip reqs diff --git a/dev/travis-build.sh b/dev/travis-build.sh index e4e5138..79a782c 100644 --- a/dev/travis-build.sh +++ b/dev/travis-build.sh @@ -19,6 +19,6 @@ export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" -pyenv shell miniconda3-3.19.0/envs/fomod-editor +pyenv shell miniconda3-3.19.0/envs/fomod-designer invoke build diff --git a/dev/vagrant-bootstrap.sh b/dev/vagrant-bootstrap.sh index 3727ee5..78e2f9b 100644 --- a/dev/vagrant-bootstrap.sh +++ b/dev/vagrant-bootstrap.sh @@ -103,11 +103,11 @@ env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install miniconda3-3.19.0 # make the virtualenv pyenv shell miniconda3-3.19.0 -conda create -y -n fomod-editor \ +conda create -y -n fomod-designer \ -c https://conda.anaconda.org/mmcauliffe \ -c https://conda.anaconda.org/anaconda \ python=3.5.1 pyqt5=5.5.1 lxml=3.5.0 -pyenv shell miniconda3-3.19.0/envs/fomod-editor +pyenv shell miniconda3-3.19.0/envs/fomod-designer # move to the project folder and install the pip reqs diff --git a/fomod/exceptions.py b/fomod/exceptions.py index 0f22dfe..d0bd960 100644 --- a/fomod/exceptions.py +++ b/fomod/exceptions.py @@ -32,7 +32,7 @@ def excepthook(exc_type, exc_value, tracebackobj): notice = ( "An unhandled exception occurred. Please report the problem" - " at Github," + " at Github," " Nexus or" " STEP.") version_info = __version__ From 613b73a14d2afe388988ef0492e21ec37bffaff6 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 20 May 2016 01:03:59 +0000 Subject: [PATCH 08/27] Improved .settings file parsing. --- dev/vagrant-bootstrap.sh | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/dev/vagrant-bootstrap.sh b/dev/vagrant-bootstrap.sh index 78e2f9b..d3905f2 100644 --- a/dev/vagrant-bootstrap.sh +++ b/dev/vagrant-bootstrap.sh @@ -54,19 +54,25 @@ export PS1="\[\033[38;5;10m\]\u@ \$(parse_git_branch)\w\\$ \[$(tput sgr0)\]" python3 - < Date: Fri, 20 May 2016 01:30:22 +0000 Subject: [PATCH 09/27] Fixed relation between View menu and visiblity state of docked widgets. --- fomod/gui.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/fomod/gui.py b/fomod/gui.py index c018707..7faa38d 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -65,9 +65,13 @@ def __init__(self): self.actionHe_lp.triggered.connect(self.help) self.action_About.triggered.connect(self.about) self.actionClear.triggered.connect(self.clear_recent_files) - self.action_Object_Tree.toggled.connect(self.toggle_tree) - self.actionObject_Box.toggled.connect(self.toggle_list) - self.action_Property_Editor.toggled.connect(self.toggle_editor) + self.action_Object_Tree.toggled.connect(self.object_tree.setVisible) + self.actionObject_Box.toggled.connect(self.object_box.setVisible) + self.action_Property_Editor.toggled.connect(self.property_editor.setVisible) + + self.object_tree.visibilityChanged.connect(self.action_Object_Tree.setChecked) + self.object_box.visibilityChanged.connect(self.actionObject_Box.setChecked) + self.property_editor.visibilityChanged.connect(self.action_Property_Editor.setChecked) self.object_tree_view.clicked.connect(self.selected_object_tree) self.object_box_list.activated.connect(self.selected_object_list) @@ -205,24 +209,6 @@ def about(self): about_dialog = About(self) about_dialog.exec_() - def toggle_tree(self, visible): - if visible: - self.object_tree.show() - else: - self.object_tree.hide() - - def toggle_list(self, visible): - if visible: - self.object_box.show() - else: - self.object_box.hide() - - def toggle_editor(self, visible): - if visible: - self.property_editor.show() - else: - self.property_editor.hide() - def clear_recent_files(self): config = ConfigParser() config.read_dict(read_settings()) From 5e1c0a322449e6348cfa950b6f257bfc66229b4a Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sat, 28 May 2016 01:14:53 +0000 Subject: [PATCH 10/27] Updated file and window icons. Fixed a small glitch when exiting a wizard. --- fomod/gui.py | 5 +++-- resources/file_icon.ico | Bin 67254 -> 208958 bytes resources/window_icon.jpg | Bin 21878 -> 0 bytes resources/window_icon.svg | 46 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) delete mode 100644 resources/window_icon.jpg create mode 100644 resources/window_icon.svg diff --git a/fomod/gui.py b/fomod/gui.py index 7faa38d..e793127 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -43,7 +43,7 @@ def __init__(self): self.setupUi(self) # setup the icons properly - self.setWindowIcon(QIcon(join(cur_folder, "resources/window_icon.jpg"))) + self.setWindowIcon(QIcon(join(cur_folder, "resources/window_icon.svg"))) self.action_Open.setIcon(QIcon(join(cur_folder, "resources/logos/logo_open_file.png"))) self.action_Save.setIcon(QIcon(join(cur_folder, "resources/logos/logo_floppy_disk.png"))) self.actionO_ptions.setIcon(QIcon(join(cur_folder, "resources/logos/logo_gear.png"))) @@ -156,7 +156,7 @@ def open(self, path=""): def save(self): try: - if not self.info_root and not self.config_root: + if self.info_root is not None and self.config_root is not None: return elif self.fomod_changed: sort_elements(self.info_root, self.config_root) @@ -491,6 +491,7 @@ def close(): self.splitter.insertWidget(0, wizard) wizard.cancelled.connect(close) + wizard.cancelled.connect(lambda: self.selected_object_tree(current_index)) wizard.finished.connect(close) wizard.finished.connect(lambda: self.selected_object_tree(current_index)) wizard.finished.connect(lambda: self.fomod_modified(True)) diff --git a/resources/file_icon.ico b/resources/file_icon.ico index 3dc390e3f28377fccabaf55a73f409f29b7b8f52..73a5c829076ab3d3af0ffd62896cb5013fa7876c 100644 GIT binary patch literal 208958 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 USb@!yt^fc407*qoM6N<$f)>^>iU0rr diff --git a/resources/window_icon.jpg b/resources/window_icon.jpg deleted file mode 100644 index 86742d5de58a1f8b295abf18230f28b4207aee1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21878 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 diff --git a/resources/window_icon.svg b/resources/window_icon.svg new file mode 100644 index 0000000..aacab92 --- /dev/null +++ b/resources/window_icon.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 66723a0ad1ac84a9b21c08bfa962844b3d48cf2c Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 29 May 2016 19:06:09 +0000 Subject: [PATCH 11/27] Added intro screen. Dialog windows are now correctly set on top of the main window. --- fomod/__main__.py | 5 +- fomod/gui.py | 64 +++++++++++-- resources/templates/intro.ui | 161 ++++++++++++++++++++++---------- resources/templates/settings.ui | 102 +++++++++++++++++--- 4 files changed, 262 insertions(+), 70 deletions(-) diff --git a/fomod/__main__.py b/fomod/__main__.py index cbadfe4..5c816f4 100644 --- a/fomod/__main__.py +++ b/fomod/__main__.py @@ -17,15 +17,14 @@ import sys from PyQt5 import QtWidgets from .exceptions import excepthook -from .gui import MainFrame +from .gui import IntroWindow def main(): sys.excepthook = excepthook app = QtWidgets.QApplication(sys.argv) - window = MainFrame() - window.show() + IntroWindow() sys.exit(app.exec_()) diff --git a/fomod/gui.py b/fomod/gui.py index e793127..d396c7c 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -19,7 +19,7 @@ from datetime import datetime from configparser import ConfigParser from PyQt5.uic import loadUiType -from PyQt5.QtWidgets import (QShortcut, QFileDialog, QColorDialog, QMessageBox, QLabel, QHBoxLayout, +from PyQt5.QtWidgets import (QShortcut, QFileDialog, QColorDialog, QMessageBox, QLabel, QHBoxLayout, QCommandLinkButton, QFormLayout, QLineEdit, QSpinBox, QComboBox, QWidget, QPushButton) from PyQt5.QtGui import QIcon, QPixmap, QStandardItemModel, QColor from PyQt5.QtCore import Qt, pyqtSignal @@ -30,11 +30,53 @@ from .exceptions import GenericError +intro_ui = loadUiType(join(cur_folder, "resources/templates/intro.ui")) base_ui = loadUiType(join(cur_folder, "resources/templates/mainframe.ui")) settings_ui = loadUiType(join(cur_folder, "resources/templates/settings.ui")) about_ui = loadUiType(join(cur_folder, "resources/templates/about.ui")) +class IntroWindow(intro_ui[0], intro_ui[1]): + def __init__(self): + super().__init__() + self.setupUi(self) + self.setWindowIcon(QIcon(join(cur_folder, "resources/window_icon.svg"))) + self.setWindowTitle("FOMOD Designer") + self.version.setText("Version " + __version__) + + settings = read_settings() + for key in sorted(settings["Recent Files"]): + if settings["Recent Files"][key]: + butto = QCommandLinkButton(basename(settings["Recent Files"][key]), settings["Recent Files"][key], self) + butto.clicked.connect(lambda _, path=settings["Recent Files"][key]: self.open_path(path)) + self.scroll_layout.addWidget(butto) + + if not settings["General"]["show_intro"]: + main_window = MainFrame() + main_window.move(self.pos()) + main_window.show() + self.close() + else: + self.show() + + self.new_button.clicked.connect(lambda: self.open_path("")) + + def open_path(self, path): + config = ConfigParser() + config.read_dict(read_settings()) + config["General"]["show_intro"] = str(not self.check_intro.isChecked()).lower() + config["General"]["show_advanced"] = str(self.check_advanced.isChecked()).lower() + makedirs(join(expanduser("~"), ".fomod"), exist_ok=True) + with open(join(expanduser("~"), ".fomod", ".designer"), "w") as configfile: + config.write(configfile) + + main_window = MainFrame() + main_window.move(self.pos()) + main_window.open(path) + main_window.show() + self.close() + + class MainFrame(base_ui[0], base_ui[1]): xml_code_changed = pyqtSignal([object]) @@ -181,7 +223,7 @@ def save(self): return def options(self): - config = SettingsDialog() + config = SettingsDialog(self) config.exec_() self.settings_dict = read_settings() @@ -554,11 +596,11 @@ def closeEvent(self, event): class SettingsDialog(settings_ui[0], settings_ui[1]): - def __init__(self): - super().__init__() + def __init__(self, parent): + super().__init__(parent=parent) self.setupUi(self) - self.setWindowFlags(Qt.WindowSystemMenuHint | Qt.WindowTitleHint) + self.setWindowFlags(Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.Dialog) self.buttonBox.accepted.connect(self.accepted) self.buttonBox.rejected.connect(self.rejected) @@ -569,6 +611,8 @@ def __init__(self): config = read_settings() self.combo_code_refresh.setCurrentIndex(config["General"]["code_refresh"]) + self.check_intro.setChecked(config["General"]["show_intro"]) + self.check_advanced.setChecked(config["General"]["show_advanced"]) self.check_valid_load.setChecked(config["Load"]["validate"]) self.check_valid_load_ignore.setChecked(config["Load"]["validate_ignore"]) self.check_warn_load.setChecked(config["Load"]["warnings"]) @@ -587,6 +631,8 @@ def accepted(self): config = ConfigParser() config.read_dict(read_settings()) config["General"]["code_refresh"] = str(self.combo_code_refresh.currentIndex()) + config["General"]["show_intro"] = str(self.check_intro.isChecked()).lower() + config["General"]["show_advanced"] = str(self.check_advanced.isChecked()).lower() config["Load"]["validate"] = str(self.check_valid_load.isChecked()).lower() config["Load"]["validate_ignore"] = str(self.check_valid_load_ignore.isChecked()).lower() config["Load"]["warnings"] = str(self.check_warn_load.isChecked()).lower() @@ -632,12 +678,12 @@ def update_warn_save(self, new_state): class About(about_ui[0], about_ui[1]): def __init__(self, parent): - super().__init__() + super().__init__(parent=parent) self.setupUi(self) self.move(parent.window().frameGeometry().topLeft() + parent.window().rect().center() - self.rect().center()) - self.setWindowFlags(Qt.WindowTitleHint) + self.setWindowFlags(Qt.WindowTitleHint | Qt.Dialog) self.version.setText("Version: " + __version__) @@ -663,7 +709,9 @@ def generic_errorbox(title, text, detail=""): def read_settings(): - default_settings = {"General": {"code_refresh": 3}, + default_settings = {"General": {"code_refresh": 3, + "show_intro": True, + "show_advanced": False}, "Load": {"validate": True, "validate_ignore": False, "warnings": True, diff --git a/resources/templates/intro.ui b/resources/templates/intro.ui index 3d6f132..3b895cf 100644 --- a/resources/templates/intro.ui +++ b/resources/templates/intro.ui @@ -41,7 +41,7 @@ 20 - 34 + 40 @@ -86,6 +86,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -94,7 +107,7 @@ 20 - 24 + 40 @@ -126,7 +139,7 @@ false - New + New/Open @@ -145,6 +158,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -161,16 +187,41 @@ - - - - 0 - 0 - - - - Open - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Don't show this window again. + + + + + + + false + + + Show the Advanced View at startup. + + + false + + + + @@ -196,7 +247,7 @@ 20 - 24 + 40 @@ -209,8 +260,15 @@ 0 + + + 75 + true + true + + - Recent Packages + Recent Packages: Qt::AlignCenter @@ -218,48 +276,55 @@ - - - - - Qt::Horizontal - - - - 40 - 20 - - - - + - + - + 0 0 - - - 16777215 - 130 - + + QAbstractScrollArea::AdjustToContentsOnFirstShow - - - - - - Qt::Horizontal + + true - - - 40 - 20 - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - + + + + 0 + 0 + 587 + 69 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + diff --git a/resources/templates/settings.ui b/resources/templates/settings.ui index 5d4f5b0..ed7867a 100644 --- a/resources/templates/settings.ui +++ b/resources/templates/settings.ui @@ -13,7 +13,7 @@ 0 0 565 - 240 + 261 @@ -44,6 +44,89 @@ false + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 20 + 20 + + + + + + + + Show intro window at statup. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 20 + 20 + + + + + + + + false + + + Show the Advanced View at startup. + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -65,7 +148,7 @@ - XML Code Refresh Rate + Preview Refresh Rate @@ -145,9 +228,9 @@ - + - + Qt::Horizontal @@ -199,9 +282,9 @@ - + - + Qt::Horizontal @@ -284,7 +367,7 @@ - + 6 @@ -299,9 +382,6 @@ Qt::Horizontal - - QSizePolicy::Expanding - 40 @@ -353,7 +433,7 @@ - + From b5b61c02ada4730d64e547f2a3c8975559607e52 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 29 May 2016 19:22:27 +0000 Subject: [PATCH 12/27] Improved node names. Fixed intro window's recent files scroll area, should now have a max expansion. About window is now called through a staticmethod. --- fomod/gui.py | 9 +++--- fomod/nodes.py | 4 +-- resources/templates/about.ui | 24 +++++++++++----- resources/templates/intro.ui | 55 ++++++++++++++++++++++++++++++++---- 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/fomod/gui.py b/fomod/gui.py index d396c7c..23d5d58 100644 --- a/fomod/gui.py +++ b/fomod/gui.py @@ -60,6 +60,7 @@ def __init__(self): self.show() self.new_button.clicked.connect(lambda: self.open_path("")) + self.button_about.clicked.connect(lambda _, self_=self: MainFrame.about(self_)) def open_path(self, path): config = ConfigParser() @@ -105,7 +106,7 @@ def __init__(self): self.action_Delete.triggered.connect(self.delete) self.delete_sec_shortcut.activated.connect(self.delete) self.actionHe_lp.triggered.connect(self.help) - self.action_About.triggered.connect(self.about) + self.action_About.triggered.connect(lambda _, self_=self: self.about(self_)) self.actionClear.triggered.connect(self.clear_recent_files) self.action_Object_Tree.toggled.connect(self.object_tree.setVisible) self.actionObject_Box.toggled.connect(self.object_box.setVisible) @@ -246,9 +247,9 @@ def delete(self): def help(): not_implemented() - def about(self): - # noinspection PyTypeChecker - about_dialog = About(self) + @staticmethod + def about(parent): + about_dialog = About(parent) about_dialog.exec_() def clear_recent_files(self): diff --git a/fomod/nodes.py b/fomod/nodes.py index cbf1ebf..76bee0a 100644 --- a/fomod/nodes.py +++ b/fomod/nodes.py @@ -174,7 +174,7 @@ class NodeInfoGroup(_NodeBase): def _init(self): allowed_child = (NodeInfoElement,) - self.init("Group", type(self).tag, 1, allowed_children=allowed_child) + self.init("Categories Group", type(self).tag, 1, allowed_children=allowed_child) super()._init() @@ -182,7 +182,7 @@ class NodeInfoElement(_NodeBase): tag = "element" def _init(self): - self.init("Element", type(self).tag, 0, allow_text=True) + self.init("Category", type(self).tag, 0, allow_text=True) super()._init() diff --git a/resources/templates/about.ui b/resources/templates/about.ui index 3198447..5291e58 100644 --- a/resources/templates/about.ui +++ b/resources/templates/about.ui @@ -9,8 +9,8 @@ 0 0 - 287 - 351 + 334 + 355 @@ -21,14 +21,14 @@ - 287 - 351 + 334 + 355 - 287 - 351 + 334 + 355 @@ -98,6 +98,16 @@ + + + + Special Thanks to Hishutup. + + + Qt::AlignCenter + + + @@ -116,7 +126,7 @@ false - Qt::AlignCenter + Qt::AlignJustify|Qt::AlignVCenter true diff --git a/resources/templates/intro.ui b/resources/templates/intro.ui index 3b895cf..34cd7a5 100644 --- a/resources/templates/intro.ui +++ b/resources/templates/intro.ui @@ -158,6 +158,43 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + About + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -280,31 +317,37 @@ - + 0 0 + + + 16777215 + 158 + + - QAbstractScrollArea::AdjustToContentsOnFirstShow + QAbstractScrollArea::AdjustIgnored true - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 0 - 0 + 26 587 - 69 + 16 - + 0 0 From 0f611d1ab9496699eb1b6d3999033e20a8715ecb Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Wed, 8 Jun 2016 15:25:46 +0000 Subject: [PATCH 13/27] Upgraded validator, downgraded PyInstaller, fixed readme badges. --- README.md | 2 +- dev/appveyor-bootstrap.bat | 1 - dev/reqs.txt | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f254c3e..e7658df 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # FOMOD Designer -[![Build status](https://ci.appveyor.com/api/projects/status/nep4id3ammekof68?svg=true)](https://ci.appveyor.com/project/GandaG/fomod-editor) [![Build Status](https://travis-ci.org/GandaG/fomod-editor.svg?branch=develop)](https://travis-ci.org/GandaG/fomod-editor) +[![Build status](https://ci.appveyor.com/api/projects/status/nep4id3ammekof68?svg=true)](https://ci.appveyor.com/project/GandaG/fomod-designer) [![Build Status](https://travis-ci.org/GandaG/fomod-designer.svg?branch=develop)](https://travis-ci.org/GandaG/fomod-designer) *A visual editor to quickly create FOMOD installers for Nexus based mods.* diff --git a/dev/appveyor-bootstrap.bat b/dev/appveyor-bootstrap.bat index 7855a30..9bff0e2 100644 --- a/dev/appveyor-bootstrap.bat +++ b/dev/appveyor-bootstrap.bat @@ -4,7 +4,6 @@ set PATH=C:\Miniconda-x64;C:\Miniconda-x64\Scripts;%PATH% conda create -y -n fomod-designer^ -c https://conda.anaconda.org/mmcauliffe^ - -c https://conda.anaconda.org/anaconda^ pyqt5=5.5.1 python=3.5.1 lxml=3.5.0 call activate fomod-designer diff --git a/dev/reqs.txt b/dev/reqs.txt index 72374d2..8641d69 100644 --- a/dev/reqs.txt +++ b/dev/reqs.txt @@ -1,6 +1,6 @@ bumpversion==0.5.3 -fomod-validator==1.2.0 +fomod-validator==1.3.0 invoke==0.12.2 lxml==3.5.0 Pygments==2.1.3 -PyInstaller==3.2 +PyInstaller==3.1.1 From fbc15ef501f1a7915b51bc3274731c26089d9ae1 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Wed, 8 Jun 2016 16:20:05 +0000 Subject: [PATCH 14/27] Updated file icon. Added status tips to the interface. --- README.md | 2 +- resources/file_icon.ico | Bin 208958 -> 33762 bytes resources/templates/mainframe.ui | 70 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e7658df..0c86cb8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # FOMOD Designer -[![Build status](https://ci.appveyor.com/api/projects/status/nep4id3ammekof68?svg=true)](https://ci.appveyor.com/project/GandaG/fomod-designer) [![Build Status](https://travis-ci.org/GandaG/fomod-designer.svg?branch=develop)](https://travis-ci.org/GandaG/fomod-designer) +[![Build status](https://ci.appveyor.com/api/projects/status/nep4id3ammekof68?svg=true)](https://ci.appveyor.com/project/GandaG/fomod-editor) [![Build Status](https://travis-ci.org/GandaG/fomod-designer.svg?branch=develop)](https://travis-ci.org/GandaG/fomod-designer) *A visual editor to quickly create FOMOD installers for Nexus based mods.* diff --git a/resources/file_icon.ico b/resources/file_icon.ico index 73a5c829076ab3d3af0ffd62896cb5013fa7876c..7f2c44116f388f3d4c199a6ecac08824dc802e6a 100644 GIT binary patch literal 33762 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 js>@G1{rjcvd Qt::Horizontal + + true + 0 @@ -96,6 +99,9 @@ + + A list of all the properties and the means to edit them. + Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea @@ -144,10 +150,29 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + + + true + + + The tree containing all the existing nodes. + false @@ -200,6 +225,9 @@ + + A list with the possible nodes to add. + Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea @@ -243,6 +271,9 @@ &File + + Your recent files in an easy-to-open list. + &Recent Files @@ -295,6 +326,9 @@ New/Open + + Select a folder to open or create an installer there. + Ctrl+O @@ -313,6 +347,9 @@ &Save + + Save the current installer. + Ctrl+S @@ -328,6 +365,15 @@ O&ptions + + Settings + + + Settings + + + Open up the settings menu + Ctrl+P @@ -343,6 +389,9 @@ &Refresh + + Refresh the previews. + Ctrl+R @@ -358,6 +407,9 @@ &Delete + + Delete the currently selected node. + Ctrl+D @@ -382,6 +434,9 @@ About + + Who made this and license details. + Ctrl+A @@ -397,6 +452,9 @@ He&lp + + Haaaaaalp! + Ctrl+H @@ -414,6 +472,9 @@ Object &Tree + + Toggle the visibility of the Object Tree. + @@ -425,6 +486,9 @@ &Property Editor + + Toggle the visibility of the Property Editor. + @@ -436,6 +500,9 @@ Object &Box + + Toggle the visibility of the Object Box. + @@ -446,6 +513,9 @@ Clear + + Clear all the recent files. + From 52974344a02a669684b4cb16536bfa2404f129db Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Wed, 8 Jun 2016 17:37:09 +0000 Subject: [PATCH 15/27] Updated files wizard. Added item template to files wizard. --- fomod/wizards.py | 82 ++++++----------------- resources/templates/wizard_files_01.ui | 14 ++-- resources/templates/wizard_files_item.ui | 83 ++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 68 deletions(-) create mode 100644 resources/templates/wizard_files_item.ui diff --git a/fomod/wizards.py b/fomod/wizards.py index 08cbb9a..9f6af54 100644 --- a/fomod/wizards.py +++ b/fomod/wizards.py @@ -38,21 +38,18 @@ def __init__(self, parent, element, main_window): if type(self) is _WizardBase: raise BaseInstanceException(self) - self.return_data = None self.element = element self.parent = parent self.main_window = main_window - self.pages = self._add_pages() - for page in self.pages: - self.addWidget(page) + self._setup_pages() @abstractmethod def _process_results(self, result): pass @abstractmethod - def _add_pages(self): - return [] + def _setup_pages(self): + pass class WizardFiles(_WizardBase): @@ -64,7 +61,7 @@ def _process_results(self, result): item_parent.insertRow(row, result.model_item) self.finished.emit() - def _add_pages(self): + def _setup_pages(self): def add_elem(element_, layout): """ :param element_: The element to be copied @@ -100,7 +97,7 @@ def add_elem(element_, layout): page.finish_button.clicked.connect(lambda: self._process_results(element_result)) page.cancel_button.clicked.connect(self.cancelled.emit) - return [page] + self.addWidget(page) def _create_field(self, element, parent_widget): """ @@ -113,66 +110,29 @@ def button_clicked(): if element.tag == "file": file_path = open_dialog.getOpenFileName(self, "Select File:", self.main_window.package_path) if file_path[0]: - edit_source.setText(relpath(file_path[0], self.main_window.package_path)) + item.edit_source.setText(relpath(file_path[0], self.main_window.package_path)) elif element.tag == "folder": folder_path = open_dialog.getExistingDirectory(self, "Select folder:", self.main_window.package_path) if folder_path: - edit_source.setText(relpath(folder_path, self.main_window.package_path)) + item.edit_source.setText(relpath(folder_path, self.main_window.package_path)) parent_element = element.getparent() - # main widget - base = QWidget(parent_widget) - layout_main = QHBoxLayout(base) - layout_main.setContentsMargins(0, 0, 0, 0) - - # the entire source form, label + (edit + button) - layout_source = QHBoxLayout() - layout_source.setContentsMargins(0, 0, 0, 0) - label_source = QLabel("Source:", base) - - # the source box (edit + button) - base_source = QWidget(base) - layout_source_field = QHBoxLayout(base_source) - layout_source_field.setContentsMargins(0, 0, 0, 0) - edit_source = QLineEdit(element.get("source"), base_source) - button_source = QPushButton("...", base_source) - button_source.setMaximumSize(50, 30) - layout_source_field.addWidget(edit_source) - layout_source_field.addWidget(button_source) - - # finish the source form - layout_source.addWidget(label_source) - layout_source.addWidget(base_source) - - # the entire destination form, label + (edit + button) - layout_dest = QHBoxLayout() - layout_dest.setContentsMargins(0, 0, 0, 0) - label_dest = QLabel("Destination:", base) - edit_dest = QLineEdit(element.get("destination"), base) - layout_dest.addWidget(label_dest) - layout_dest.addWidget(edit_dest) + item = loadUi(join(cur_folder, "resources/templates/wizard_files_item.ui")) # the delete self button - button_delete = QPushButton(QIcon(join(cur_folder, "resources/logos/logo_cross.png")), "", base) - button_delete.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - button_delete.setMaximumSize(24, 24) - - # finish the main widget - layout_main.addLayout(layout_source) - layout_main.addLayout(layout_dest) - layout_main.addWidget(button_delete) + item.button_delete.setIcon(QIcon(join(cur_folder, "resources/logos/logo_cross.png"))) # connect the signals - edit_source.textChanged.connect(element.properties["source"].set_value) - edit_source.textChanged.connect(element.write_attribs) - edit_source.textChanged.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) - edit_dest.textChanged.connect(element.properties["destination"].set_value) - edit_dest.textChanged.connect(element.write_attribs) - edit_dest.textChanged.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) - button_source.clicked.connect(button_clicked) - button_delete.clicked.connect(base.deleteLater) - button_delete.clicked.connect(lambda x: parent_element.remove_child(element)) - button_delete.clicked.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) - - return base + item.edit_source.textChanged.connect(element.properties["source"].set_value) + item.edit_source.textChanged.connect(element.write_attribs) + item.edit_source.textChanged.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) + item.edit_dest.textChanged.connect(element.properties["destination"].set_value) + item.edit_dest.textChanged.connect(element.write_attribs) + item.edit_dest.textChanged.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) + item.button_source.clicked.connect(button_clicked) + item.button_delete.clicked.connect(item.deleteLater) + item.button_delete.clicked.connect(lambda x: parent_element.remove_child(element)) + item.button_delete.clicked.connect(lambda: self.main_window.xml_code_changed.emit(parent_element)) + + return item diff --git a/resources/templates/wizard_files_01.ui b/resources/templates/wizard_files_01.ui index d79f7aa..c8666cb 100644 --- a/resources/templates/wizard_files_01.ui +++ b/resources/templates/wizard_files_01.ui @@ -48,7 +48,7 @@ - <html><head/><body><p align="justify">Select the files and folders to be installed. The &quot;<span style=" font-style:italic;">Source&quot;</span> fields are used to select the file/folder to be installed. The <span style=" font-style:italic;">&quot;Destination&quot;</span> fields are used to select where the item will be installed to - a blank fields corresponds to the <span style=" font-style:italic;">Data</span> folder itself.</p><p align="justify">See the <span style=" text-decoration: underline;">Help</span> for more information.</p><p align="justify"><span style=" font-weight:600;">Note:</span> When a folder is selected, it will be recursively installed - all the files and folders under it, and so on.</p></body></html> + <html><head/><body><p align="justify">Select the files and folders to be installed. The &quot;<span style=" font-style:italic;">Source&quot;</span> fields are used to select the file/folder to be installed. The <span style=" font-style:italic;">&quot;Destination&quot;</span> fields are used to select where the item will be installed to - a blank fields corresponds to the <span style=" font-style:italic;">Data</span> folder itself.</p><p align="justify">See the <span style=" text-decoration: underline;">Help</span> for more information.</p></body></html> Qt::AutoText @@ -105,18 +105,18 @@ 0 0 431 - 106 + 125 - 0 + 3 0 - 0 + 3 0 @@ -176,18 +176,18 @@ 0 0 431 - 106 + 125 - 0 + 3 0 - 0 + 3 0 diff --git a/resources/templates/wizard_files_item.ui b/resources/templates/wizard_files_item.ui new file mode 100644 index 0000000..6969077 --- /dev/null +++ b/resources/templates/wizard_files_item.ui @@ -0,0 +1,83 @@ + + + base + + + + 0 + 0 + 396 + 24 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Source: + + + + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + ... + + + + + + + Destination: + + + + + + + + + + + + + + ../logos/logo_cross.png../logos/logo_cross.png + + + + + + + + From 12d2b14afa41651f4917df8ac3da37166d08944f Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Wed, 8 Jun 2016 19:02:55 +0000 Subject: [PATCH 16/27] Fixed initial files wizard code preview. Fixed files wizard initial fields not being populated. --- fomod/wizards.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/fomod/wizards.py b/fomod/wizards.py index 9f6af54..9f01db1 100644 --- a/fomod/wizards.py +++ b/fomod/wizards.py @@ -72,7 +72,10 @@ def add_elem(element_, layout): child.properties[key].set_value(element_.attrib[key]) element_result.add_child(child) spacer = layout.takeAt(layout.count() - 1) - layout.addWidget(self._create_field(child, page)) + item = self._create_field(child) + item.edit_source.setText(child.properties["source"].value) + item.edit_dest.setText(child.properties["destination"].value) + layout.addWidget(item) layout.addSpacerItem(spacer) self.main_window.xml_code_changed.emit(element_result) @@ -82,13 +85,13 @@ def add_elem(element_, layout): file_list = [elem for elem in element_result if elem.tag == "file"] for element in file_list: - add_elem(element, page.layout_file) element_result.remove_child(element) + add_elem(element, page.layout_file) folder_list = [elem for elem in element_result if elem.tag == "folder"] for element in folder_list: - add_elem(element, page.layout_folder) element_result.remove_child(element) + add_elem(element, page.layout_folder) # finish with connections page.button_add_file.clicked.connect(lambda: add_elem(elem_factory("file", element_result), page.layout_file)) @@ -99,10 +102,9 @@ def add_elem(element_, layout): self.addWidget(page) - def _create_field(self, element, parent_widget): + def _create_field(self, element): """ :param element: the element newly copied - :param parent_widget: the parent widget (the QWidgets inside the scroll areas) :return: base QWidget, with the source and destination fields built """ def button_clicked(): From b5de33c212a36b29b47f7bfee6be6ecd56c06f3b Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 9 Jun 2016 16:10:53 +0000 Subject: [PATCH 17/27] Renamed source folder. Moved code preview to new module. --- {fomod => designer}/__init__.py | 0 {fomod => designer}/__main__.py | 0 {fomod => designer}/exceptions.py | 0 {fomod => designer}/gui.py | 3 ++- {fomod => designer}/io.py | 14 +------------- {fomod => designer}/nodes.py | 0 designer/previews.py | 29 +++++++++++++++++++++++++++++ {fomod => designer}/props.py | 0 {fomod => designer}/wizards.py | 0 dev/pyinstaller-bootstrap.py | 2 +- 10 files changed, 33 insertions(+), 15 deletions(-) rename {fomod => designer}/__init__.py (100%) rename {fomod => designer}/__main__.py (100%) rename {fomod => designer}/exceptions.py (100%) rename {fomod => designer}/gui.py (99%) rename {fomod => designer}/io.py (94%) rename {fomod => designer}/nodes.py (100%) create mode 100644 designer/previews.py rename {fomod => designer}/props.py (100%) rename {fomod => designer}/wizards.py (100%) diff --git a/fomod/__init__.py b/designer/__init__.py similarity index 100% rename from fomod/__init__.py rename to designer/__init__.py diff --git a/fomod/__main__.py b/designer/__main__.py similarity index 100% rename from fomod/__main__.py rename to designer/__main__.py diff --git a/fomod/exceptions.py b/designer/exceptions.py similarity index 100% rename from fomod/exceptions.py rename to designer/exceptions.py diff --git a/fomod/gui.py b/designer/gui.py similarity index 99% rename from fomod/gui.py rename to designer/gui.py index 23d5d58..ae17bcf 100644 --- a/fomod/gui.py +++ b/designer/gui.py @@ -25,7 +25,8 @@ from PyQt5.QtCore import Qt, pyqtSignal from validator import validate_tree, check_warnings, ValidatorError, ValidationError, WarningError from . import cur_folder, __version__ -from .io import import_, new, export, sort_elements, elem_factory, highlight_fragment +from .io import import_, new, export, sort_elements, elem_factory +from .previews import highlight_fragment from .props import PropertyFile, PropertyColour, PropertyFolder, PropertyCombo, PropertyInt, PropertyText from .exceptions import GenericError diff --git a/fomod/io.py b/designer/io.py similarity index 94% rename from fomod/io.py rename to designer/io.py index 2d5e9b0..9a01aca 100644 --- a/fomod/io.py +++ b/designer/io.py @@ -17,11 +17,7 @@ from os import listdir, makedirs from os.path import join from lxml.etree import (PythonElementClassLookup, XMLParser, tostring, fromstring, - Element, SubElement, parse, ParseError, ElementTree, XML) -from lxml.objectify import deannotate -from pygments import highlight -from pygments.formatters.html import HtmlFormatter -from pygments.lexers.html import XmlLexer + Element, SubElement, parse, ParseError, ElementTree) from .exceptions import MissingFileError, ParserError, TagNotFound @@ -242,14 +238,6 @@ def export(info_root, config_root, package_path): config_tree.write(configfile, pretty_print=True) -def highlight_fragment(element): - element.write_attribs() - new_elem = XML(tostring(element)) - deannotate(new_elem, cleanup_namespaces=True) - code = tostring(new_elem, encoding="Unicode", pretty_print=True, xml_declaration=False) - return highlight(code, XmlLexer(), HtmlFormatter(noclasses=True, style="autumn")) - - def sort_elements(info_root, config_root): for root in (info_root, config_root): for parent in root.xpath('//*[./*]'): diff --git a/fomod/nodes.py b/designer/nodes.py similarity index 100% rename from fomod/nodes.py rename to designer/nodes.py diff --git a/designer/previews.py b/designer/previews.py new file mode 100644 index 0000000..0007a1a --- /dev/null +++ b/designer/previews.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# Copyright 2016 Daniel Nunes +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from lxml.etree import XML, tostring +from lxml.objectify import deannotate +from pygments import highlight +from pygments.formatters.html import HtmlFormatter +from pygments.lexers.html import XmlLexer + + +def highlight_fragment(element): + element.write_attribs() + new_elem = XML(tostring(element)) + deannotate(new_elem, cleanup_namespaces=True) + code = tostring(new_elem, encoding="Unicode", pretty_print=True, xml_declaration=False) + return highlight(code, XmlLexer(), HtmlFormatter(noclasses=True, style="autumn")) diff --git a/fomod/props.py b/designer/props.py similarity index 100% rename from fomod/props.py rename to designer/props.py diff --git a/fomod/wizards.py b/designer/wizards.py similarity index 100% rename from fomod/wizards.py rename to designer/wizards.py diff --git a/dev/pyinstaller-bootstrap.py b/dev/pyinstaller-bootstrap.py index 8a6b988..4060baa 100644 --- a/dev/pyinstaller-bootstrap.py +++ b/dev/pyinstaller-bootstrap.py @@ -19,5 +19,5 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) if __name__ == '__main__': - from fomod.__main__ import main # placed here so pycharm doesn't complain about import location + from designer.__main__ import main # placed here so pycharm doesn't complain about import location main() From c62b5c88d813b2bc5012d7b498ef30f8c4c03aed Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 9 Jun 2016 18:26:09 +0000 Subject: [PATCH 18/27] Changed object box from a list to independent buttons. --- designer/gui.py | 66 +++++++++++++++++--------------- resources/templates/mainframe.ui | 39 +++++++++++++++---- 2 files changed, 67 insertions(+), 38 deletions(-) diff --git a/designer/gui.py b/designer/gui.py index ae17bcf..5380636 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -20,8 +20,8 @@ from configparser import ConfigParser from PyQt5.uic import loadUiType from PyQt5.QtWidgets import (QShortcut, QFileDialog, QColorDialog, QMessageBox, QLabel, QHBoxLayout, QCommandLinkButton, - QFormLayout, QLineEdit, QSpinBox, QComboBox, QWidget, QPushButton) -from PyQt5.QtGui import QIcon, QPixmap, QStandardItemModel, QColor + QFormLayout, QLineEdit, QSpinBox, QComboBox, QWidget, QPushButton, QFrame, QSizePolicy) +from PyQt5.QtGui import QIcon, QPixmap, QStandardItemModel, QColor, QFont from PyQt5.QtCore import Qt, pyqtSignal from validator import validate_tree, check_warnings, ValidatorError, ValidationError, WarningError from . import cur_folder, __version__ @@ -118,7 +118,6 @@ def __init__(self): self.property_editor.visibilityChanged.connect(self.action_Property_Editor.setChecked) self.object_tree_view.clicked.connect(self.selected_object_tree) - self.object_box_list.activated.connect(self.selected_object_list) self.wizard_button.clicked.connect(self.run_wizard) self.xml_code_changed.connect(self.update_gen_code) @@ -132,15 +131,12 @@ def __init__(self): self.config_root = None self.current_object = None self.current_prop_list = [] - self.current_children_list = [] self.tree_model = QStandardItemModel() - self.list_model = QStandardItemModel() self.wizard_button.hide() self.object_tree_view.setModel(self.tree_model) self.object_tree_view.header().hide() - self.object_box_list.setModel(self.list_model) self.update_recent_files() @@ -328,14 +324,44 @@ def selected_object_tree(self, index): self.update_wizard_button() def update_box_list(self): - self.list_model.clear() - self.current_children_list.clear() + for index in reversed(range(self.layout_box.count())): + widget = self.layout_box.takeAt(index).widget() + if widget is not None: + widget.deleteLater() for child in self.current_object.allowed_children: new_object = child() if self.current_object.can_add_child(new_object): - self.list_model.appendRow(new_object.model_item) - self.current_children_list.append(new_object) + child_button = QPushButton(new_object.name) + child_button.setFlat(True) + font_button = QFont() + font_button.setPointSize(8) + child_button.setFont(font_button) + child_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + child_button.clicked.connect(lambda _, tag_=new_object.tag: self.selected_object_list(tag_)) + self.layout_box.addWidget(child_button) + if self.current_object.allowed_children.index(child) != len(self.current_object.allowed_children) - 1: + line = QFrame() + line.setFrameShape(4) + self.layout_box.addWidget(line) + + def selected_object_list(self, tag): + new_child = elem_factory(tag, self.current_object) + self.current_object.add_child(new_child) + + # expand the parent + current_index = self.tree_model.indexFromItem(self.current_object.model_item) + self.object_tree_view.expand(current_index) + + # select the new item + self.object_tree_view.setCurrentIndex(self.tree_model.indexFromItem(new_child.model_item)) + self.selected_object_tree(self.tree_model.indexFromItem(new_child.model_item)) + + # reload the object box + self.update_box_list() + + # set the installer as changed + self.fomod_modified(True) def clear_prop_list(self): self.current_prop_list.clear() @@ -540,26 +566,6 @@ def close(): wizard.finished.connect(lambda: self.selected_object_tree(current_index)) wizard.finished.connect(lambda: self.fomod_modified(True)) - def selected_object_list(self, index): - item = self.list_model.itemFromIndex(index) - - new_child = elem_factory(item.xml_node.tag, self.current_object) - self.current_object.add_child(new_child) - - # expand the parent - current_index = self.tree_model.indexFromItem(self.current_object.model_item) - self.object_tree_view.expand(current_index) - - # select the new item - self.object_tree_view.setCurrentIndex(self.tree_model.indexFromItem(new_child.model_item)) - self.selected_object_tree(self.tree_model.indexFromItem(new_child.model_item)) - - # reload the object box - self.update_box_list() - - # set the installer as changed - self.fomod_modified(True) - def update_gen_code(self, element): if element is not None: self.xml_code_browser.setHtml(highlight_fragment(element)) diff --git a/resources/templates/mainframe.ui b/resources/templates/mainframe.ui index 09134e1..71d1c21 100644 --- a/resources/templates/mainframe.ui +++ b/resources/templates/mainframe.ui @@ -99,6 +99,12 @@ + + + 250 + 59 + + A list of all the properties and the means to edit them. @@ -139,6 +145,11 @@ 0 + + + 8 + + Start Wizard @@ -170,6 +181,12 @@ true + + + 250 + 93 + + The tree containing all the existing nodes. @@ -225,6 +242,12 @@ + + + 250 + 83 + + A list with the possible nodes to add. @@ -238,22 +261,22 @@ 2 - + + + 3 + - 5 + 3 - 0 + 3 - 5 + 3 - 0 + 3 - - - From 6f6761d1014b657dccbbb634b79f77e4b9f80d94 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 21:19:14 +0000 Subject: [PATCH 19/27] Simplified validation process. Fixed recent files' order. Fixed settings parsing when there are more options than expected. --- designer/gui.py | 58 +++++++++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/designer/gui.py b/designer/gui.py index 5380636..8244ad7 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -23,7 +23,7 @@ QFormLayout, QLineEdit, QSpinBox, QComboBox, QWidget, QPushButton, QFrame, QSizePolicy) from PyQt5.QtGui import QIcon, QPixmap, QStandardItemModel, QColor, QFont from PyQt5.QtCore import Qt, pyqtSignal -from validator import validate_tree, check_warnings, ValidatorError, ValidationError, WarningError +from validator import validate_tree, check_warnings, ValidatorError from . import cur_folder, __version__ from .io import import_, new, export, sort_elements, elem_factory from .previews import highlight_fragment @@ -159,20 +159,11 @@ def open(self, path=""): if package_path: info_root, config_root = import_(normpath(package_path)) if info_root is not None and config_root is not None: - try: - if self.settings_dict["Load"]["validate"]: - validate_tree(config_root, join(cur_folder, "resources", "mod_schema.xsd")) - except ValidationError as e: - generic_errorbox(e.title, str(e)) - if not self.settings_dict["Load"]["validate_ignore"]: - return - try: - if self.settings_dict["Load"]["warnings"]: - check_warnings(package_path, config_root) - except WarningError as e: - generic_errorbox(e.title, str(e)) - if not self.settings_dict["Load"]["warn_ignore"]: - return + if self.settings_dict["Load"]["validate"]: + validate_tree(config_root, join(cur_folder, "resources", "mod_schema.xsd"), + self.settings_dict["Load"]["validate_ignore"]) + if self.settings_dict["Load"]["warnings"]: + check_warnings(package_path, config_root, self.settings_dict["Save"]["warn_ignore"]) else: info_root, config_root = new() @@ -200,20 +191,11 @@ def save(self): return elif self.fomod_changed: sort_elements(self.info_root, self.config_root) - try: - if self.settings_dict["Save"]["validate"]: - validate_tree(self.config_root, join(cur_folder, "resources", "mod_schema.xsd")) - except ValidationError as e: - generic_errorbox(e.title, str(e)) - if not self.settings_dict["Save"]["validate_ignore"]: - return - try: - if self.settings_dict["Save"]["warnings"]: - check_warnings(self.package_path, self.config_root) - except WarningError as e: - generic_errorbox(e.title, str(e)) - if not self.settings_dict["Save"]["warn_ignore"]: - return + if self.settings_dict["Save"]["validate"]: + validate_tree(self.config_root, join(cur_folder, "resources", "mod_schema.xsd"), + self.settings_dict["Save"]["validate_ignore"]) + if self.settings_dict["Save"]["warnings"]: + check_warnings(self.package_path, self.config_root, self.settings_dict["Save"]["warn_ignore"]) export(self.info_root, self.config_root, self.package_path) self.fomod_modified(False) except ValidatorError as e: @@ -285,12 +267,20 @@ def invalid_path(path_): file_list = [] settings = read_settings() - for index in range(1, 5): + + # Populate the file_list with the existing recent files + for index in range(1, len(settings["Recent Files"])): if settings["Recent Files"]["file" + str(index)]: file_list.append(settings["Recent Files"]["file" + str(index)]) - file_list = sorted(set(file_list)) # remove all duplicates there was an issue with duplicate after invalid path + + # remove all duplicates there was an issue with duplicate after invalid path + seen = set() + seen_add = seen.add + file_list = [x for x in file_list if not (x in seen or seen_add(x))] + self.clear_recent_files() + # check if the path is new or if it already exists - delete the last one or reorder respectively if add_new: if add_new in file_list: file_list.remove(add_new) @@ -298,6 +288,7 @@ def invalid_path(path_): file_list.pop() file_list.insert(0, add_new) + # write the new list to the settings file config = ConfigParser() config.read_dict(settings) for path in file_list: @@ -306,6 +297,7 @@ def invalid_path(path_): with open(join(expanduser("~"), ".fomod", ".designer"), "w") as configfile: config.write(configfile) + # populate the gui menu with the new files list self.menu_Recent_Files.removeAction(self.actionClear) for path in file_list: action = self.menu_Recent_Files.addAction(path) @@ -738,9 +730,9 @@ def read_settings(): config.read(join(expanduser("~"), ".fomod", ".designer")) settings = {} - for section in config: + for section in default_settings: settings[section] = {} - for key in config[section]: + for key in default_settings[section]: if isinstance(default_settings[section][key], bool): settings[section][key] = config.getboolean(section, key) elif isinstance(default_settings[section][key], int): From c02cc0a939938d7f2f4e5e4a65b7213fb6ca50a5 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 22:59:55 +0000 Subject: [PATCH 20/27] Added documentation to gui module. --- designer/gui.py | 138 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 7 deletions(-) diff --git a/designer/gui.py b/designer/gui.py index 8244ad7..3323eb3 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -38,6 +38,9 @@ class IntroWindow(intro_ui[0], intro_ui[1]): + """ + The class for the intro window. Subclassed from QDialog and created in Qt Designer. + """ def __init__(self): super().__init__() self.setupUi(self) @@ -64,6 +67,11 @@ def __init__(self): self.button_about.clicked.connect(lambda _, self_=self: MainFrame.about(self_)) def open_path(self, path): + """ + Method used to open a path in the main window - closes the intro window and show the main. + + :param path: The path to open. + """ config = ConfigParser() config.read_dict(read_settings()) config["General"]["show_intro"] = str(not self.check_intro.isChecked()).lower() @@ -80,6 +88,11 @@ def open_path(self, path): class MainFrame(base_ui[0], base_ui[1]): + """ + The class for the main window. Subclassed from QMainWindow and created in Qt Designer. + """ + + # The signal to update the previews. xml_code_changed = pyqtSignal([object]) def __init__(self): @@ -141,6 +154,14 @@ def __init__(self): self.update_recent_files() def open(self, path=""): + """ + Open a new installer if one exists at path (if no path is given a dialog pops up asking the user to choose one) + or create a new one. + + If enabled in the Settings the installer is also validated and checked for common errors. + + :param path: Optional. The path to open/create an installer at. + """ try: answer = self.check_fomod_state() if answer == QMessageBox.Save: @@ -186,6 +207,11 @@ def open(self, path=""): return def save(self): + """ + Saves the current installer at the current path. + + If enabled in the Settings the installer is also validated and checked for common errors. + """ try: if self.info_root is not None and self.config_root is not None: return @@ -203,15 +229,24 @@ def save(self): return def options(self): + """ + Opens the Settings dialog. + """ config = SettingsDialog(self) config.exec_() self.settings_dict = read_settings() def refresh(self): + """ + Refreshes all the previews if the refresh rate in Settings is high enough. + """ if self.settings_dict["General"]["code_refresh"] >= 1: self.xml_code_changed.emit(self.current_object) def delete(self): + """ + Deletes the current node in the tree. No effect when using the Basic View. + """ try: if self.current_object is not None: object_to_delete = self.current_object @@ -228,10 +263,18 @@ def help(): @staticmethod def about(parent): + """ + Opens the About dialog. This method is static to be able to be called from the Intro window. + + :param parent: The parent of the dialog. + """ about_dialog = About(parent) about_dialog.exec_() def clear_recent_files(self): + """ + Clears the Recent Files gui menu and settings. + """ config = ConfigParser() config.read_dict(read_settings()) for key in config["Recent Files"]: @@ -246,7 +289,19 @@ def clear_recent_files(self): del child def update_recent_files(self, add_new=None): + """ + Updates the Recent Files gui menu and settings. If called when opening an installer, pass that installer as + add_new so it can be added to list or placed at the top. + + :param add_new: If a new installer is being opened, add it to the list or move it to the top. + """ def invalid_path(path_): + """ + Called when a Recent Files path in invalid. Requests user decision on wether to delete the item or to leave + it. + + :param path_: The invalid path. + """ msg_box = QMessageBox() msg_box.setWindowTitle("This path no longer exists.") msg_box.setText("Remove it from the Recent Files list?") @@ -306,6 +361,14 @@ def invalid_path(path_): self.menu_Recent_Files.addAction(self.actionClear) def selected_object_tree(self, index): + """ + Called when the user selects a node in the Object Tree. + + Updates the current object, emits the preview update signal if Settings allows it, + updates the possible children list, the properties list and wizard buttons. + + :param index: The selected node's index. + """ self.current_object = self.tree_model.itemFromIndex(index).xml_node self.object_tree_view.setCurrentIndex(index) if self.settings_dict["General"]["code_refresh"] >= 2: @@ -316,6 +379,9 @@ def selected_object_tree(self, index): self.update_wizard_button() def update_box_list(self): + """ + Updates the possible children to add in Object Box. + """ for index in reversed(range(self.layout_box.count())): widget = self.layout_box.takeAt(index).widget() if widget is not None: @@ -338,6 +404,13 @@ def update_box_list(self): self.layout_box.addWidget(line) def selected_object_list(self, tag): + """ + Called when the user selects a possible child in the Object Box. + + Adds the child corresponding to the tag and updates the possible children list. + + :param tag: The tag of the child to add. + """ new_child = elem_factory(tag, self.current_object) self.current_object.add_child(new_child) @@ -356,6 +429,9 @@ def selected_object_list(self, tag): self.fomod_modified(True) def clear_prop_list(self): + """ + Deletes all the properties from the Property Editor + """ self.current_prop_list.clear() for index in reversed(range(self.formLayout.count())): widget = self.formLayout.takeAt(index).widget() @@ -363,6 +439,9 @@ def clear_prop_list(self): widget.deleteLater() def update_props_list(self): + """ + Updates the Property Editor's prop list. Deletes everything and then creates the list from the node's properties. + """ self.clear_prop_list() prop_index = 0 @@ -523,12 +602,20 @@ def update_button_colour(text): prop_index += 1 def update_wizard_button(self): + """ + Updates the wizard button, hides or shows it. + """ if self.current_object.wizard: self.wizard_button.show() else: self.wizard_button.hide() def run_wizard(self): + """ + Called when the wizard button is clicked. + + Sets up the main window and runs the wizard. + """ def close(): wizard.deleteLater() self.action_Object_Tree.toggled.emit(enabled_tree) @@ -559,12 +646,20 @@ def close(): wizard.finished.connect(lambda: self.fomod_modified(True)) def update_gen_code(self, element): + """ + Updates the previews. + + :param element: The element to preview. + """ if element is not None: self.xml_code_browser.setHtml(highlight_fragment(element)) else: self.xml_code_browser.setText("") def fomod_modified(self, changed): + """ + Changes the modified state of the installer, according to the parameter. + """ if changed is False: self.fomod_changed = False self.setWindowTitle(self.package_name + " - " + self.original_title) @@ -573,6 +668,9 @@ def fomod_modified(self, changed): self.setWindowTitle("*" + self.package_name + " - " + self.original_title) def check_fomod_state(self): + """ + Checks whether the installer has unsaved changes. + """ if self.fomod_changed: msg_box = QMessageBox() msg_box.setWindowTitle("The installer has been modified.") @@ -586,16 +684,23 @@ def check_fomod_state(self): return def closeEvent(self, event): - answer = self.check_fomod_state() - if answer == QMessageBox.Save: - self.save() - elif answer == QMessageBox.Discard: - pass - elif answer == QMessageBox.Cancel: - event.ignore() + """ + Override the Qt close event to account for unsaved changes. + :param event: + """ + answer = self.check_fomod_state() + if answer == QMessageBox.Save: + self.save() + elif answer == QMessageBox.Discard: + pass + elif answer == QMessageBox.Cancel: + event.ignore() class SettingsDialog(settings_ui[0], settings_ui[1]): + """ + The class for the settings window. Subclassed from QDialog and created in Qt Designer. + """ def __init__(self, parent): super().__init__(parent=parent) self.setupUi(self) @@ -677,6 +782,9 @@ def update_warn_save(self, new_state): class About(about_ui[0], about_ui[1]): + """ + The class for the about window. Subclassed from QDialog and created in Qt Designer. + """ def __init__(self, parent): super().__init__(parent=parent) self.setupUi(self) @@ -696,10 +804,20 @@ def __init__(self, parent): def not_implemented(): + """ + A convenience function for something that has not yet been implemented. + """ generic_errorbox("Nope", "Sorry, this part hasn't been implemented yet!") def generic_errorbox(title, text, detail=""): + """ + A function that creates a generic errorbox with the logo_admin.png logo. + + :param title: A string containing the title of the errorbox. + :param text: A string containing the text of the errorbox. + :param detail: Optional. A string containing the detail text of the errorbox. + """ errorbox = QMessageBox() errorbox.setText(text) errorbox.setWindowTitle(title) @@ -709,6 +827,12 @@ def generic_errorbox(title, text, detail=""): def read_settings(): + """ + Reads the settings from the ~/.fomod/.designer file. If such a file does not exist it uses the default settings. + The settings are processed to be ready to be used in Python code (p.e. "option=1" translates to True). + + :return: The processed settings. + """ default_settings = {"General": {"code_refresh": 3, "show_intro": True, "show_advanced": False}, From 89f560bd23a9e061368a3cc82077641c00ecaa8a Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 01:42:38 +0000 Subject: [PATCH 21/27] Added documentation to the exception module. --- designer/exceptions.py | 25 +++++++++++++++++++++---- designer/gui.py | 4 ++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/designer/exceptions.py b/designer/exceptions.py index d0bd960..686983f 100644 --- a/designer/exceptions.py +++ b/designer/exceptions.py @@ -52,14 +52,20 @@ def excepthook(exc_type, exc_value, tracebackobj): errorbox.exec_() -class GenericError(Exception): +class DesignerError(Exception): + """ + Base class for all exceptions. + """ def __init__(self): self.title = "Generic Error" self.detailed = "" Exception.__init__(self, "Something happened...") -class MissingFileError(GenericError): +class MissingFileError(DesignerError): + """ + Exception raised when the export/import functions could not find a file/folder. + """ def __init__(self, fname): self.title = "I/O Error" self.message = "{} is missing.".format(fname.capitalize()) @@ -67,7 +73,12 @@ def __init__(self, fname): Exception.__init__(self, self.message) -class ParserError(GenericError): +class ParserError(DesignerError): + """ + Exception raised when the parser was unable to properly parse the file. + + It tries to locate the line where the error occurred if lxml provides it. + """ def __init__(self, msg): self.title = "Parser Error" if len(msg.split(",")) <= 2: @@ -80,7 +91,10 @@ def __init__(self, msg): Exception.__init__(self, self.msg) -class TagNotFound(GenericError): +class TagNotFound(DesignerError): + """ + Exception raised when the element factory did not match the element tag. + """ def __init__(self, element): self.title = "Tag Lookup Error" self.message = "Tag {} at line {} could not be matched.".format(element.tag, element.sourceline) @@ -88,6 +102,9 @@ def __init__(self, element): class BaseInstanceException(Exception): + """ + Exception raised when trying to instanced base classes (not meant to be used). + """ def __init__(self, base_instance): self.title = "Instance Error" self.message = "{} is not meant to be instanced. A subclass should be used instead.".format(type(base_instance)) diff --git a/designer/gui.py b/designer/gui.py index 3323eb3..22744db 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -28,7 +28,7 @@ from .io import import_, new, export, sort_elements, elem_factory from .previews import highlight_fragment from .props import PropertyFile, PropertyColour, PropertyFolder, PropertyCombo, PropertyInt, PropertyText -from .exceptions import GenericError +from .exceptions import DesignerError intro_ui = loadUiType(join(cur_folder, "resources/templates/intro.ui")) @@ -202,7 +202,7 @@ def open(self, path=""): self.xml_code_changed.emit(self.current_object) self.update_recent_files(self.package_path) self.clear_prop_list() - except (GenericError, ValidatorError) as p: + except (DesignerError, ValidatorError) as p: generic_errorbox(p.title, str(p), p.detailed) return From 24878169ff51f1697c615b0233be39e776de9f86 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 02:22:43 +0000 Subject: [PATCH 22/27] Added documentation to the io module. --- designer/io.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/designer/io.py b/designer/io.py index 9a01aca..d30f4bd 100644 --- a/designer/io.py +++ b/designer/io.py @@ -20,8 +20,13 @@ Element, SubElement, parse, ParseError, ElementTree) from .exceptions import MissingFileError, ParserError, TagNotFound +module_parser = XMLParser(remove_comments=True, remove_pis=True, remove_blank_text=True) + class _NodeLookup(PythonElementClassLookup): + """ + Class that handles the custom lookup for the element factories. + """ def lookup(self, doc, element): from . import nodes @@ -118,11 +123,17 @@ def lookup(self, doc, element): raise TagNotFound(element) -module_parser = XMLParser(remove_comments=True, remove_pis=True, remove_blank_text=True) module_parser.set_element_class_lookup(_NodeLookup()) def _check_file(base_path, file_): + """ + Function used to search case-insensitively for a file/folder is a given path. + + :param base_path: The path to search for the file/folder in. + :param file_: The file/folder to search for. + :return: The file if found, raises an exception if not. + """ base_file = file_ try: for item in listdir(base_path): @@ -134,6 +145,12 @@ def _check_file(base_path, file_): def _validate_child(child): + """ + Function used during installer import to check if each element's children is valid. + + :param child: The child to check. + :return: True if valid, False if not. + """ if type(child) in child.getparent().allowed_children: if child.allowed_instances: instances = 0 @@ -148,6 +165,17 @@ def _validate_child(child): def elem_factory(tag, parent): + """ + Function meant as a replacement for the default element factory. + + Creates a tree up to the root, re-parses that tree and returns an element corresponding to the tag given. + This is necessary due to the way the _NodeLookup class works when checking for tags + (it requires parents and grandparents). + + :param tag: The tag to create an element from. + :param parent: The parent of the future element. + :return: The created element with the tag *tag*. + """ list_ = [parent] for elem in parent.iterancestors(): list_.append(elem) @@ -166,6 +194,14 @@ def elem_factory(tag, parent): def import_(package_path): + """ + Function used to import an existing installer from *package_path*. + + Raises ``ParserError`` if the lxml parser could not read a file. + + :param package_path: The package where the installer is. + :return: The root elements of each installer file. A tuple of None, None if any file is missing. + """ try: fomod_folder = _check_file(package_path, "fomod") fomod_folder_path = join(package_path, fomod_folder) @@ -199,6 +235,9 @@ def import_(package_path): def new(): + """ + Creates and returns new root nodes for each element. + """ from . import nodes info_root = module_parser.makeelement(nodes.NodeInfoRoot.tag) @@ -208,6 +247,13 @@ def new(): def export(info_root, config_root, package_path): + """ + Exports the root elements and saves them to installer files. + + :param info_root: The root element of the info.xml file. + :param config_root: The root element of the moduleconfig.xml file. + :param package_path: The path to save the files to. + """ try: fomod_folder = _check_file(package_path, "fomod") except MissingFileError as e: @@ -239,6 +285,12 @@ def export(info_root, config_root, package_path): def sort_elements(info_root, config_root): + """ + Sorts the xml elements according to their sort_order member. + + :param info_root: The root element of the info.xml file. + :param config_root: The root element of the moduleconfig.xml file. + """ for root in (info_root, config_root): for parent in root.xpath('//*[./*]'): parent[:] = sorted(parent, key=lambda x: x.sort_order) From b40ad8ab93fe376ca65dace678252209bdd593ca Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 15:15:39 +0000 Subject: [PATCH 23/27] Added documentation to the previews module. --- designer/io.py | 2 +- designer/previews.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/designer/io.py b/designer/io.py index d30f4bd..e1494da 100644 --- a/designer/io.py +++ b/designer/io.py @@ -287,7 +287,7 @@ def export(info_root, config_root, package_path): def sort_elements(info_root, config_root): """ Sorts the xml elements according to their sort_order member. - + :param info_root: The root element of the info.xml file. :param config_root: The root element of the moduleconfig.xml file. """ diff --git a/designer/previews.py b/designer/previews.py index 0007a1a..4b87873 100644 --- a/designer/previews.py +++ b/designer/previews.py @@ -22,6 +22,12 @@ def highlight_fragment(element): + """ + Takes a xml element, writes the code, highlights it with inline css and returns the html code. + + :param element: The elements to highlight. Includes the element's children. + :return: The highlighted element code. + """ element.write_attribs() new_elem = XML(tostring(element)) deannotate(new_elem, cleanup_namespaces=True) From 32a82991e9b04ee502b8ffe0fc63a8d63e599133 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 15:32:24 +0000 Subject: [PATCH 24/27] Added documentation to the props module. --- designer/props.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/designer/props.py b/designer/props.py index e1fc904..6c48b67 100644 --- a/designer/props.py +++ b/designer/props.py @@ -18,7 +18,15 @@ class _PropertyBase(object): + """ + Base class for the properties. Shouldn't be used directly. + """ def __init__(self, name, values, editable=True): + """ + :param name: The display name of the variable. + :param values: The acceptable values tuple. + :param editable: If the property is editable. If not, it will not be displayed. + """ if type(self) is _PropertyBase: raise BaseInstanceException(self) @@ -29,17 +37,28 @@ def __init__(self, name, values, editable=True): self.values = values def set_value(self, value): + """ + Method used to set the property's value. Sub-classes should validate the value before setting it. + + :param value: The value to be validated and set. + """ if self.editable: self.value = value class PropertyText(_PropertyBase): + """ + A property that holds simple text. + """ def __init__(self, name, text="", editable=True): super().__init__(name, (), editable) self.value = text class PropertyCombo(_PropertyBase): + """ + A property that holds a combo list - only one value from this list should be selected. + """ def __init__(self, name, values, editable=True): super().__init__(name, values, editable) self.value = values[0] @@ -50,7 +69,17 @@ def set_value(self, value): class PropertyInt(_PropertyBase): + """ + A property that holds an integer. + """ def __init__(self, name, min_value, max_value, default, editable=True): + """ + :param name: The display name of the variable. + :param min_value: The minimum integer value. + :param max_value: The maximum integer value. + :param default: The default value for the property. + :param editable: If the property is editable. If not, it will not be displayed. + """ self.min = min_value self.max = max_value values = range(min_value, max_value + 1) @@ -64,18 +93,27 @@ def set_value(self, value): class PropertyFolder(_PropertyBase): + """ + A property that holds the path to a folder. + """ def __init__(self, name, text="", editable=True): super().__init__(name, (), editable) self.value = text class PropertyFile(_PropertyBase): + """ + A property that holds the path to a file. + """ def __init__(self, name, text="", editable=True): super().__init__(name, (), editable) self.value = text class PropertyColour(_PropertyBase): + """ + A property that holds a colour hex value. + """ def __init__(self, name, text="", editable=True): super().__init__(name, (), editable) self.value = text From 6ec1a10bfe236ca7c8f87c291fabb9a31d20167e Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 17:12:45 +0000 Subject: [PATCH 25/27] Added documentation to the wizards module. --- designer/wizards.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/designer/wizards.py b/designer/wizards.py index 9f01db1..8d2854f 100644 --- a/designer/wizards.py +++ b/designer/wizards.py @@ -17,8 +17,7 @@ from abc import ABCMeta, abstractmethod from copy import deepcopy from os.path import join, relpath -from PyQt5.QtWidgets import (QHBoxLayout, QWidget, QPushButton, QSizePolicy, - QStackedWidget, QLineEdit, QLabel, QFileDialog) +from PyQt5.QtWidgets import QStackedWidget, QFileDialog from PyQt5.QtGui import QIcon from PyQt5.QtCore import pyqtSignal from PyQt5.uic import loadUi @@ -28,12 +27,20 @@ class _WizardBase(QStackedWidget): + """ + The base class for wizards. Shouldn't be instantiated directly. + """ __metaclass__ = ABCMeta cancelled = pyqtSignal() finished = pyqtSignal() def __init__(self, parent, element, main_window): + """ + :param parent: The parent widget. + :param element: The element this wizard corresponds. + :param main_window: The app's main window. + """ super().__init__(parent) if type(self) is _WizardBase: raise BaseInstanceException(self) @@ -45,14 +52,25 @@ def __init__(self, parent, element, main_window): @abstractmethod def _process_results(self, result): + """ + Method called to process the results into a new element. + + :param result: The temporary element with all the info. + """ pass @abstractmethod def _setup_pages(self): + """ + Method called during initialization to create all the pages necessary for each wizard. + """ pass class WizardFiles(_WizardBase): + """ + Wizard fo the "files" tag. + """ def _process_results(self, result): self.element.getparent().replace(self.element, result) item_parent = self.element.model_item.parent() From 18558fb6ac3aa6c3e49e24a14a58ea3986931b72 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 17:40:33 +0000 Subject: [PATCH 26/27] Added documentation to the nodes module. --- designer/nodes.py | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/designer/nodes.py b/designer/nodes.py index 76bee0a..8d160c3 100644 --- a/designer/nodes.py +++ b/designer/nodes.py @@ -23,6 +23,9 @@ class _NodeBase(etree.ElementBase): + """ + The base class for all nodes. Should never be instantiated directly. + """ def _init(self): if type(self) is _NodeBase: raise BaseInstanceException(self) @@ -50,6 +53,12 @@ def init(self, name, tag, allowed_instances, sort_order=0, allow_text=False, all self.model_item.setEditable(False) def can_add_child(self, child): + """ + Checks if the given child can be added to this node. + + :param child: The child to check. + :return: True if possible, False if not. + """ if child.allowed_instances: instances = 0 for item in self: @@ -62,17 +71,30 @@ def can_add_child(self, child): return False def add_child(self, child): + """ + Adds the given child to this node. Includes a check with can_add_child at the start. + + :param child: The child to add. + """ if self.can_add_child(child): self.append(child) self.model_item.appendRow(child.model_item) child.write_attribs() def remove_child(self, child): + """ + Removes the given child from this node. + + :param child: The child to remove. + """ if child in self: self.model_item.takeRow(child.model_item.row()) self.remove(child) def parse_attribs(self): + """ + Reads the values from the BaseElement's attrib dictionary into the node's properties. + """ for key in self.properties: if key not in self.attrib.keys(): continue @@ -80,11 +102,20 @@ def parse_attribs(self): self.update_item_name() def write_attribs(self): + """ + Writes the values from the node's properties into the BaseElement's attrib dictionary. + """ self.attrib.clear() for key in self.properties: self.set(key, str(self.properties[key].value)) def update_item_name(self): + """ + Updates this node's item's display name. + + If the node contains a property called "name" then it uses its value for the display. + If it contains a property called "source" then it expects a path and uses the last part of the path. + """ if "name" in self.properties: if not self.properties["name"].value: self.model_item.setText(self.name) @@ -100,6 +131,11 @@ def update_item_name(self): self.model_item.setText(self.name) def set_text(self, text): + """ + Method used to set the node's text, if allowed. + + :param text: The text to set. + """ if self.allow_text: self.text = text @@ -112,6 +148,9 @@ def __init__(self, node): class NodeInfoRoot(_NodeBase): + """ + A node for the tag fomod + """ tag = "fomod" def _init(self): @@ -122,6 +161,9 @@ def _init(self): class NodeInfoName(_NodeBase): + """ + A node for the tag Name + """ tag = "Name" def _init(self): @@ -130,6 +172,9 @@ def _init(self): class NodeInfoAuthor(_NodeBase): + """ + A node for the tag Author + """ tag = "Author" def _init(self): @@ -138,6 +183,9 @@ def _init(self): class NodeInfoVersion(_NodeBase): + """ + A node for the tag Version + """ tag = "Version" def _init(self): @@ -146,6 +194,9 @@ def _init(self): class NodeInfoID(_NodeBase): + """ + A node for the tag Id + """ tag = "Id" def _init(self): @@ -154,6 +205,9 @@ def _init(self): class NodeInfoWebsite(_NodeBase): + """ + A node for the tag Website + """ tag = "Website" def _init(self): @@ -162,6 +216,9 @@ def _init(self): class NodeInfoDescription(_NodeBase): + """ + A node for the tag Description + """ tag = "Description" def _init(self): @@ -170,6 +227,9 @@ def _init(self): class NodeInfoGroup(_NodeBase): + """ + A node for the tag Groups + """ tag = "Groups" def _init(self): @@ -179,6 +239,9 @@ def _init(self): class NodeInfoElement(_NodeBase): + """ + A node for the tag element + """ tag = "element" def _init(self): @@ -187,6 +250,9 @@ def _init(self): class NodeConfigRoot(_NodeBase): + """ + A node for the tag config + """ tag = "config" def _init(self): @@ -199,6 +265,9 @@ def _init(self): class NodeConfigModName(_NodeBase): + """ + A node for the tag moduleName + """ tag = "moduleName" def _init(self): @@ -209,6 +278,9 @@ def _init(self): class NodeConfigModImage(_NodeBase): + """ + A node for the tag moduleImage + """ tag = "moduleImage" def _init(self): @@ -220,6 +292,9 @@ def _init(self): class NodeConfigModDepend(_NodeBase): + """ + A node for the tag moduleDependencies + """ tag = "moduleDependencies" def _init(self): @@ -231,6 +306,9 @@ def _init(self): class NodeConfigReqFiles(_NodeBase): + """ + A node for the tag requiredInstallFiles + """ tag = "requiredInstallFiles" def _init(self): @@ -241,6 +319,9 @@ def _init(self): class NodeConfigInstallSteps(_NodeBase): + """ + A node for the tag installSteps + """ tag = "installSteps" def _init(self): @@ -252,6 +333,9 @@ def _init(self): class NodeConfigCondInstall(_NodeBase): + """ + A node for the tag conditionalFileInstalls + """ tag = "conditionalFileInstalls" def _init(self): @@ -261,6 +345,9 @@ def _init(self): class NodeConfigDependFile(_NodeBase): + """ + A node for the tag fileDependency + """ tag = "fileDependency" def _init(self): @@ -271,6 +358,9 @@ def _init(self): class NodeConfigDependFlag(_NodeBase): + """ + A node for the tag flagDependency + """ tag = "flagDependency" def _init(self): @@ -280,6 +370,9 @@ def _init(self): class NodeConfigDependGame(_NodeBase): + """ + A node for the tag gameDependency + """ tag = "gameDependency" def _init(self): @@ -289,6 +382,9 @@ def _init(self): class NodeConfigFile(_NodeBase): + """ + A node for the tag file + """ tag = "file" def _init(self): @@ -302,6 +398,9 @@ def _init(self): class NodeConfigFolder(_NodeBase): + """ + A node for the tag folder + """ tag = "folder" def _init(self): @@ -315,6 +414,9 @@ def _init(self): class NodeConfigPatterns(_NodeBase): + """ + A node for the tag patterns + """ tag = "patterns" def _init(self): @@ -324,6 +426,9 @@ def _init(self): class NodeConfigPattern(_NodeBase): + """ + A node for the tag pattern + """ tag = "pattern" def _init(self): @@ -333,6 +438,9 @@ def _init(self): class NodeConfigFiles(_NodeBase): + """ + A node for the tag files + """ tag = "files" def _init(self): @@ -342,6 +450,9 @@ def _init(self): class NodeConfigDependencies(_NodeBase): + """ + A node for the tag dependencies + """ tag = "dependencies" def _init(self): @@ -354,6 +465,9 @@ def _init(self): class NodeConfigNestedDependencies(_NodeBase): + """ + A node for the tag dependencies (this one refers to the all the dependencies that have a dependencies as a parent). + """ tag = "dependencies" def _init(self): @@ -365,6 +479,9 @@ def _init(self): class NodeConfigInstallStep(_NodeBase): + """ + A node for the tag installStep + """ tag = "installStep" def _init(self): @@ -375,6 +492,9 @@ def _init(self): class NodeConfigVisible(_NodeBase): + """ + A node for the tag visible + """ tag = "visible" def _init(self): @@ -384,6 +504,9 @@ def _init(self): class NodeConfigOptGroups(_NodeBase): + """ + A node for the tag optionalFileGroups + """ tag = "optionalFileGroups" def _init(self): @@ -395,6 +518,9 @@ def _init(self): class NodeConfigGroup(_NodeBase): + """ + A node for the tag group + """ tag = "group" def _init(self): @@ -407,6 +533,9 @@ def _init(self): class NodeConfigPlugins(_NodeBase): + """ + A node for the tag plugins + """ tag = "plugins" def _init(self): @@ -417,6 +546,9 @@ def _init(self): class NodeConfigPlugin(_NodeBase): + """ + A node for the tag plugin + """ tag = "plugin" def _init(self): @@ -428,6 +560,9 @@ def _init(self): class NodeConfigPluginDescription(_NodeBase): + """ + A node for the tag description + """ tag = "description" def _init(self): @@ -436,6 +571,9 @@ def _init(self): class NodeConfigImage(_NodeBase): + """ + A node for the tag image + """ tag = "image" def _init(self): @@ -445,6 +583,9 @@ def _init(self): class NodeConfigConditionFlags(_NodeBase): + """ + A node for the tag conditionFlags + """ tag = "conditionFlags" def _init(self): @@ -454,6 +595,9 @@ def _init(self): class NodeConfigTypeDesc(_NodeBase): + """ + A node for the tag typeDescriptor + """ tag = "typeDescriptor" def _init(self): @@ -469,6 +613,9 @@ def can_add_child(self, child): class NodeConfigFlag(_NodeBase): + """ + A node for the tag flag + """ tag = "flag" def _init(self): @@ -478,6 +625,9 @@ def _init(self): class NodeConfigDependencyType(_NodeBase): + """ + A node for the tag dependencyType + """ tag = "dependencyType" def _init(self): @@ -487,6 +637,9 @@ def _init(self): class NodeConfigDefaultType(_NodeBase): + """ + A node for the tag defaultType + """ tag = "defaultType" def _init(self): @@ -497,6 +650,9 @@ def _init(self): class NodeConfigType(_NodeBase): + """ + A node for the tag type + """ tag = "type" def _init(self): @@ -507,6 +663,9 @@ def _init(self): class NodeConfigInstallPatterns(_NodeBase): + """ + A node for the tag patterns + """ tag = "patterns" def _init(self): @@ -516,6 +675,9 @@ def _init(self): class NodeConfigInstallPattern(_NodeBase): + """ + A node for the tag pattern + """ tag = "pattern" def _init(self): From f3024045f3f3efa0c4ec789174d34138091f721c Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 18:07:33 +0000 Subject: [PATCH 27/27] Version bump and updated changelog. --- CHANGELOG.md | 18 +++++++++++++++++- setup.cfg | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee819e2..eb4fdf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,24 @@ # Changelog +0.5.0 (2016-06-12) + +* Added intro window. +* Added Files wizard. +* Added wizard environment setup. +* Updated app and file icons. +* The object box now consists of independent buttons for each child instead of a list. +* A message box asking for confirmation should now appear when trying to open a new installer while there unsaved changes. +* Property editor should now be properly cleared when opening a new installer. +* A message box asking for action should now appear when using the recent files menu and the path no longer exists. +* Fixed relation between view menu and docked widget states. +* Dialog windows should now properly be placed on top of other windows. +* Improved some nodes' names. + +---------------------------------- + 0.4.1 (2016-05-16) -* **Urgent bugfix:** Fixed wrong default attributes in file and folder tags. +* Fixed wrong default attributes in file and folder tags. * Added wizard framework. ---------------------------------- diff --git a/setup.cfg b/setup.cfg index cda8a61..f2aa43a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.1 +current_version = 1.0.0 current_build = 0 [bdist_wheel]