diff --git a/.gitignore b/.gitignore index f2e6563..9857943 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,8 @@ coverage.xml *.log # Sphinx documentation -docs/_build/ +docs/build/ +resources/docs # PyBuilder target/ diff --git a/.travis.yml b/.travis.yml index cc350d6..45c6413 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,28 @@ language: python sudo: required dist: trusty +branches: + only: + - master + - develop + +virtualenv: + system_site_packages: true install: - chmod +x ./dev/travis-bootstrap.sh - ./dev/travis-bootstrap.sh +before_script: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" + - sleep 3 + script: + - chmod +x ./dev/travis-test.sh + - ./dev/travis-test.sh + +after_success: - chmod +x ./dev/travis-build.sh - ./dev/travis-build.sh diff --git a/CHANGELOG.md b/CHANGELOG.rst similarity index 89% rename from CHANGELOG.md rename to CHANGELOG.rst index 317d839..935e663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ -# Changelog +Changelog +========= -0.7.2 (2016-07-13) +**0.8.0 (2016-08-11)** + +* Documentation is now available. +* Users are now able to manipulate and add comments. +* Users are now able to hide non-comment nodes. +* 32 bit builds are now available. + +---------------------------------- + +**0.7.2 (2016-07-13)** * Plugin node should now have the correct required child nodes. * Fixed validation and warning dialogs and ignore process. @@ -8,14 +18,14 @@ ---------------------------------- -0.7.1 (2016-07-12) +**0.7.1 (2016-07-12)** * Fixed preview issue with non-existent nodes under info root. * Updated validation and children groups. ---------------------------------- -0.7.0 (2016-07-10) +**0.7.0 (2016-07-10)** * Fixed rare bug with the validator. * Added Dependencies Wizard. @@ -48,7 +58,7 @@ ---------------------------------- -0.6.0 (2016-06-13) +**0.6.0 (2016-06-13)** * Added check for updates at startup. * Added line numbers to code preview. @@ -57,13 +67,13 @@ ---------------------------------- -0.5.1 (2016-06-12) +**0.5.1 (2016-06-12)** * Fixed versioning issues. ---------------------------------- -0.5.0 (2016-06-12) +**0.5.0 (2016-06-12)** * Added intro window. * Added Files wizard. @@ -79,14 +89,14 @@ ---------------------------------- -0.4.1 (2016-05-16) +**0.4.1 (2016-05-16)** * Fixed wrong default attributes in file and folder tags. * Added wizard framework. ---------------------------------- -0.4.0 (2016-05-14) +**0.4.0 (2016-05-14)** * Added file and window icons. * Fixed combo boxes not being set at start. @@ -106,7 +116,7 @@ ---------------------------------- -0.3.1 (2016-04-17) +**0.3.1 (2016-04-17)** * Tags/item with name/source property now have that as the title instead of the tag's name. * Fixed all keyboard shortcuts. @@ -120,7 +130,7 @@ ---------------------------------- -0.3.0 (2016-04-07) +**0.3.0 (2016-04-07)** * All basic functionality is now done. * Tag properties are now properly displayed and editable. @@ -132,25 +142,25 @@ ---------------------------------- -0.2.1 (2016-04-05) +**0.2.1 (2016-04-05)** * In-tag text is now properly parsed and saved along with everything else. ---------------------------------- -0.2.0 (2016-04-05) +**0.2.0 (2016-04-05)** * Users can now modify the installer's objects. ---------------------------------- -0.1.0 (2016-04-03) +**0.1.0 (2016-04-03)** * Users can now open and save FOMOD installers. * Main windows title now shows which package you are currently working on. ---------------------------------- -0.0.1 (2016-03-15) +**0.0.1 (2016-03-15)** * GUI draft completed. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 4cdcca8..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,38 +0,0 @@ -# Contributing - -We love contributions from everyone. -By participating in this project, -you agree to abide by the thoughtbot [code of conduct]. - - [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct - -## Issues - -Before submitting your issue, please make sure that you've provided all the info -required in the issue template. - -## Pull Requests - -Before submitting your pull request, please make sure that you've provided all the -info required in the pull request template. - -## Contributing Code - -Fork the repo. - -Make sure the tests pass: - - tox - -Make your change, with new passing tests. Follow the [style guide][style]. - - [style]: https://www.python.org/dev/peps/pep-0008/ - -Push to your fork. Write a [good commit message][commit]. Submit a pull request. - - [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html - -Others will give constructive feedback. -This is a time for discussion and improvements, -and making the necessary changes will be required before we can -merge the contribution. \ No newline at end of file diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..a34774d --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,78 @@ +Contributing +============ + +We love contributions from everyone. +By participating in this project, +you agree to abide by the thoughtbot `code of conduct `_. + +Issues +++++++ + +Before submitting your issue, please make sure that you've provided all the info +required in the issue template. + +Pull Requests ++++++++++++++ + +Before submitting your pull request, please make sure that you've provided all the +info required in the pull request template. + +Contributing Code ++++++++++++++++++ + +**General Guidelines**: + + * This repo uses the `gitflow `_ branching model. + Don't commit directly to the ``master`` or ``develop`` branches. + + * Make sure the tests pass on the CI server. Local tests are not available at the moment. + + * Follow the `style guide `_. + + * Write `decent commit messages `_. + + * Run :command:`inv docs` to generate documentation locally, :command:`inv build` to build the executable and + :command:`inv preview` to preview the app without building it. + +**Setup the work environment**: + + 1. `Fork the repo `_. + + 2. `Setup your fork locally `_. + + 3. This repo uses a ``.settings`` file to define all the necessary settings. This file follows this syntax: + + .. code-block:: ini + + [git] + user=git_username + email=git_email + + Create and add this file to your clone's root. + + 4. Install `Vagrant `_. + + 5. Run this in the clone's root: + + * If you have Python available: + + .. code-block:: shell + + pip install invoke + inv create enter + + * If not: + + .. code-block:: shell + + vagrant up + vagrant ssh -- -Yt 'cd /vagrant/; /bin/bash' + + It will take a while. + + 6. You should now be inside an Ubuntu Trusty virtual machine, this is where you'll work. + Make, commit and push your changes. + + 7. `Create a pull request `_. + +Thank you, `contributors `_! diff --git a/README.md b/README.md index 0c86cb8..02f63da 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,11 @@ # 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) +[![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) [![Coverage Status](https://coveralls.io/repos/github/GandaG/fomod-designer/badge.svg?branch=develop)](https://coveralls.io/github/GandaG/fomod-designer?branch=develop) [![Documentation Status](https://readthedocs.org/projects/fomod-designer/badge/?version=stable)](http://fomod-designer.readthedocs.io/en/stable/?badge=stable) *A visual editor to quickly create FOMOD installers for Nexus based mods.* -## Overview +## Documentation -*TODO* - -## Installation - -* Download the zip file corresponding to your OS from the [latest release](https://github.com/GandaG/fomod-editor/releases/latest); -* Extract the folder within to a location of your choice; -* Run the "designer" executable within the folder. - -## Usage - -* Open the application; -* Click on the **New/Open** button; -* Select the root folder of your package (where the files you'll install are); -* Create/modify your installer; -* Once you're ready, click on the **Save** button to save your installer. - -## Contributing - -This repo uses a ***.settings*** file to define all the necessary settings. This file follows this syntax: - -``` -[git] -user=git_username -email=git_email -``` - -For more information see the [CONTRIBUTING] document. -Thank you, [contributors]! - - [CONTRIBUTING]: /.github/CONTRIBUTING.md - [contributors]: https://github.com/GandaG/fomod-editor/graphs/contributors +You can get more information in the official documentation hosted at [Read The Docs](http://fomod-designer.readthedocs.io/en/stable/). ## License diff --git a/appveyor.yml b/appveyor.yml index fd27717..ea6456d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,29 @@ +branches: + only: + - master + - develop + install: - .\dev\appveyor-bootstrap.bat build: off test_script: + - py.test --cov=src --cov-report html --cov-report term -vv tests/ + +after_test: + - if not exist %APPVEYOR_BUILD_FOLDER%\output mkdir %APPVEYOR_BUILD_FOLDER%\output + + - C:\Miniconda-x64\Scripts\activate.bat fomod-designer + - inv build + - ps: cp dist\*.zip output + + - C:\Miniconda\Scripts\activate.bat fomod-designer - inv build + - ps: cp dist\*.zip output artifacts: - - path: dist\* + - path: output\* name: windows_build deploy: @@ -15,7 +31,7 @@ deploy: auth_token: secure: iMaZrvVT+OI/9jRs8LyOvmzVqIBa0/jpiK96wNzZww/KqKsMcferhIeSK7faNzOo artifact: windows_build - description: '[Changelog.](https://github.com/GandaG/fomod-designer/blob/master/CHANGELOG.md)' + description: '[Changelog.](http://fomod-designer.readthedocs.io/en/stable/changelog.html)' force_update: true on: appveyor_repo_tag: true diff --git a/designer/previews.py b/designer/previews.py deleted file mode 100644 index 9cca8bf..0000000 --- a/designer/previews.py +++ /dev/null @@ -1,665 +0,0 @@ -#!/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 os.path import join, sep, normpath, isfile, isdir -from os import listdir -from queue import Queue -from PyQt5.QtCore import QThread, Qt, pyqtSignal, QEvent -from PyQt5.QtWidgets import QWidget, QLabel, QGroupBox, QVBoxLayout, QRadioButton, QCheckBox, QHeaderView, QMenu, \ - QAction -from PyQt5.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIcon -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 -from . import cur_folder -from .io import sort_elements -from .ui_templates import preview_mo - - -class PreviewDispatcherThread(QThread): - """ - Thread used to dispatch the element to each preview worker thread. - - :param queue: The main queue containing the elements to process. - :param mo_signal: The signal to pass to the MO preview worker, updates the MO preview. - :param nmm_signal: The signal to pass to the NMM preview worker, updates the NMM preview. - :param code_signal: The signal to pass to the code preview worker, updates the code preview. - """ - def __init__(self, queue, code_signal, **kwargs): - super().__init__() - self.queue = queue - self.gui_queue = Queue() - self.code_queue = Queue() - - self.code_thread = PreviewCodeWorker(self.code_queue, code_signal) - self.code_thread.start() - self.gui_thread = PreviewGuiWorker(self.gui_queue, **kwargs) - self.gui_thread.start() - - def run(self): - while True: - # wait for next element - element = self.queue.get() - - if element is not None: - element.write_attribs() - element.load_metadata() - sort_elements(element) - - # dispatch to every queue - self.gui_queue.put(element) - self.code_queue.put(element) - - -class PreviewCodeWorker(QThread): - """ - Takes a xml element, writes the code, highlights it with inline css and returns the html code. - - :param queue: The queue that receives the elements to be processed. - :param return_signal: The signal used to send the return code through. - :return: The highlighted element html code. - """ - def __init__(self, queue, return_signal): - super().__init__() - self.queue = queue - self.return_signal = return_signal - - def run(self): - while True: - # wait for next element - element = self.queue.get() - - if element is None: - self.return_signal.emit("") - continue - - element = XML(tostring(element)) - - # process the element - deannotate(element, cleanup_namespaces=True) - code = tostring(element, encoding="Unicode", pretty_print=True, xml_declaration=False) - self.return_signal.emit(highlight(code, XmlLexer(), HtmlFormatter( - noclasses=True, style="autumn", linenos="table" - ))) - - -class PreviewGuiWorker(QThread): - class InstallStepData(object): - def __init__(self, name): - self.name = name - self.group_list = [] - - def set_group_list(self, group_list): - self.group_list = group_list - - def sort_ascending(self): - self.group_list = sorted(self.group_list, key=lambda x: x.name) - - def sort_descending(self): - self.group_list = sorted(self.group_list, reverse=True, key=lambda x: x.name) - - class GroupData(object): - def __init__(self, name, group_type): - self.name = name - self.type = group_type - self.plugin_list = [] - - def set_plugin_list(self, plugin_list): - self.plugin_list = plugin_list - - def sort_ascending(self): - self.plugin_list = sorted(self.plugin_list, key=lambda x: x.name) - - def sort_descending(self): - self.plugin_list = sorted(self.plugin_list, reverse=True, key=lambda x: x.name) - - class PluginData(object): - def __init__(self, name, description, image_path, file_list, folder_list, flag_list, plugin_type): - self.name = name - self.description = description - self.image_path = image_path - self.file_list = file_list - self.folder_list = folder_list - self.flag_list = flag_list - self.type = plugin_type - - class FileData(object): - def __init__(self, abs_source, rel_source, destination, priority, always_install, install_usable): - self.abs_source = abs_source - self.rel_source = rel_source - self.destination = destination - self.priority = priority - self.always_install = always_install - self.install_usable = install_usable - - class FolderData(FileData): - pass - - class FlagData(object): - def __init__(self, label, value): - self.label = label - self.value = value - - def __init__(self, queue, **kwargs): - super().__init__() - self.queue = queue - self.kwargs = kwargs - - def run(self): - while True: - # wait for next element - element = self.queue.get() - - if element is None: - self.kwargs["gui_worker"].invalid_node_signal.emit() - continue - elif element.tag == "installStep": - pass - elif [elem for elem in element.iterancestors() if elem.tag == "installStep"]: - element = [elem for elem in element.iterancestors() if elem.tag == "installStep"][0] - elif not [elem for elem in self.kwargs["config_root"]().iter() if elem.tag == "installStep"]: - self.kwargs["gui_worker"].missing_node_signal.emit() - continue - else: - self.kwargs["gui_worker"].invalid_node_signal.emit() - continue - - self.kwargs["gui_worker"].clear_tab_signal.emit() - self.kwargs["gui_worker"].clear_ui_signal.emit() - info_name = self.kwargs["info_root"]().find("Name").text \ - if self.kwargs["info_root"]().find("Name") is not None else "" - info_author = self.kwargs["info_root"]().find("Author").text \ - if self.kwargs["info_root"]().find("Author") is not None else "" - info_version = self.kwargs["info_root"]().find("Version").text \ - if self.kwargs["info_root"]().find("Version") is not None else "" - info_website = self.kwargs["info_root"]().find("Website").text \ - if self.kwargs["info_root"]().find("Website") is not None else "" - self.kwargs["gui_worker"].set_labels_signal.emit(info_name, info_author, info_version, info_website) - - step_data = self.InstallStepData(element.get("name")) - opt_group_elem = element.find("optionalFileGroups") - if opt_group_elem is not None: - group_data_list = [] - - for group_elem in opt_group_elem.findall("group"): - group_data = self.GroupData(group_elem.get("name"), group_elem.get("type")) - - plugins_elem = group_elem.find("plugins") - if plugins_elem is not None: - plugin_data_list = [] - - for plugin_elem in plugins_elem.findall("plugin"): - name_ = plugin_elem.get("name") - description_ = plugin_elem.find("description").text \ - if plugin_elem.find("description") is not None else "" - image_ = plugin_elem.find("image").get("path") \ - if plugin_elem.find("image") is not None else "" - if image_: - # normalize path, for some reason normpath wasn't working - image_ = join(self.kwargs["package_path"](), image_).replace("\\", "/") - image_ = image_.replace("/", sep) - - file_data_list = [] - for file_elem in plugin_elem.findall("files/file"): - file_data_list.append( - self.FileData( - normpath(join( - self.kwargs["package_path"](), - file_elem.get("source").replace("\\", "/") - )), - file_elem.get("source"), - normpath(file_elem.get("destination").replace("\\", "/")), - file_elem.get("priority"), - file_elem.get("alwaysInstall"), - file_elem.get("installIfUsable") - ) - ) - - folder_data_list = [] - for folder_elem in plugin_elem.findall("files/folder"): - folder_data_list.append( - self.FolderData( - normpath(join( - self.kwargs["package_path"](), - folder_elem.get("source").replace("\\", "/") - )), - folder_elem.get("source"), - normpath(folder_elem.get("destination").replace("\\", "/")), - folder_elem.get("priority"), - folder_elem.get("alwaysInstall"), - folder_elem.get("installIfUsable") - ) - ) - - flag_data_list = [] - for flag_elem in plugin_elem.findall("conditionFlags/flag"): - flag_data_list.append( - self.FlagData( - flag_elem.get("name"), - flag_elem.text - ) - ) - - type_elem = plugin_elem.find("typeDescriptor/type") - default_type_elem = plugin_elem.find("typeDescriptor/dependencyType/defaultType") - if type_elem is not None: - type_ = type_elem.get("name") - elif default_type_elem is not None: - type_ = default_type_elem.get("name") - else: - type_ = "Required" - - plugin_data_list.append( - self.PluginData( - name_, - description_, - image_, - file_data_list, - folder_data_list, - flag_data_list, - type_ - ) - ) - - group_data.set_plugin_list(plugin_data_list) - if plugins_elem.get("order") == "Ascending": - group_data.sort_ascending() - elif plugins_elem.get("order") == "Descending": - group_data.sort_descending() - - group_data_list.append(group_data) - - step_data.set_group_list(group_data_list) - if opt_group_elem.get("order") == "Ascending": - step_data.sort_ascending() - elif opt_group_elem.get("order") == "Descending": - step_data.sort_descending() - - self.kwargs["gui_worker"].create_page_signal.emit(step_data) - - -class PreviewMoGui(QWidget, preview_mo.Ui_Form): - clear_tab_signal = pyqtSignal() - clear_ui_signal = pyqtSignal() - invalid_node_signal = pyqtSignal() - missing_node_signal = pyqtSignal() - set_labels_signal = pyqtSignal([str, str, str, str]) - create_page_signal = pyqtSignal([object]) - - class ScaledLabel(QLabel): - def __init__(self, parent=None): - super().__init__(parent) - self.original_pixmap = None - self.setMinimumSize(320, 200) - - def set_scalable_pixmap(self, pixmap): - self.original_pixmap = pixmap - self.setPixmap(self.original_pixmap.scaled(self.size(), Qt.KeepAspectRatio)) - - def resizeEvent(self, event): - if self.pixmap() and self.original_pixmap: - self.setPixmap(self.original_pixmap.scaled(event.size(), Qt.KeepAspectRatio)) - - class PreviewItem(QStandardItem): - def set_priority(self, value): - self.priority = value - - def __init__(self, mo_preview_layout): - super().__init__() - self.mo_preview_layout = mo_preview_layout - self.setupUi(self) - self.mo_preview_layout.addWidget(self) - self.label_image = self.ScaledLabel(self) - self.splitter_label.addWidget(self.label_image) - self.hide() - - self.button_preview_more.setIcon(QIcon(join(cur_folder, "resources/logos/logo_more.png"))) - self.button_preview_less.setIcon(QIcon(join(cur_folder, "resources/logos/logo_less.png"))) - self.button_preview_more.clicked.connect(self.button_preview_more.hide) - self.button_preview_more.clicked.connect(self.button_preview_less.show) - self.button_preview_more.clicked.connect(self.widget_preview.show) - self.button_preview_less.clicked.connect(self.button_preview_less.hide) - self.button_preview_less.clicked.connect(self.button_preview_more.show) - self.button_preview_less.clicked.connect(self.widget_preview.hide) - self.button_preview_more.clicked.emit() - self.button_results_more.setIcon(QIcon(join(cur_folder, "resources/logos/logo_more.png"))) - self.button_results_less.setIcon(QIcon(join(cur_folder, "resources/logos/logo_less.png"))) - self.button_results_more.clicked.connect(self.button_results_more.hide) - self.button_results_more.clicked.connect(self.button_results_less.show) - self.button_results_more.clicked.connect(self.widget_results.show) - self.button_results_less.clicked.connect(self.button_results_less.hide) - self.button_results_less.clicked.connect(self.button_results_more.show) - self.button_results_less.clicked.connect(self.widget_results.hide) - self.button_results_less.clicked.emit() - - self.model_files = QStandardItemModel() - self.tree_results.expanded.connect( - lambda: self.tree_results.header().resizeSections(QHeaderView.Stretch) - ) - self.tree_results.collapsed.connect( - lambda: self.tree_results.header().resizeSections(QHeaderView.Stretch) - ) - self.tree_results.setContextMenuPolicy(Qt.CustomContextMenu) - self.tree_results.customContextMenuRequested.connect(self.on_custom_context_menu) - self.model_flags = QStandardItemModel() - self.list_flags.expanded.connect( - lambda: self.list_flags.header().resizeSections(QHeaderView.Stretch) - ) - self.list_flags.collapsed.connect( - lambda: self.list_flags.header().resizeSections(QHeaderView.Stretch) - ) - self.reset_models() - - self.label_invalid = QLabel( - "Select an Installation Step node or one of its children to preview its installer page." - ) - self.label_invalid.setAlignment(Qt.AlignCenter) - self.mo_preview_layout.addWidget(self.label_invalid) - self.label_invalid.hide() - - self.label_missing = QLabel( - "In order to preview an installer page, create an Installation Step node." - ) - self.label_missing.setAlignment(Qt.AlignCenter) - self.mo_preview_layout.addWidget(self.label_missing) - self.label_missing.hide() - - self.clear_tab_signal.connect(self.clear_tab) - self.clear_ui_signal.connect(self.clear_ui) - self.invalid_node_signal.connect(self.invalid_node) - self.missing_node_signal.connect(self.missing_node) - self.set_labels_signal.connect(self.set_labels) - self.create_page_signal.connect(self.create_page) - - def on_custom_context_menu(self, position): - node_tree_context_menu = QMenu(self.tree_results) - - action_expand = QAction(QIcon(join(cur_folder, "resources/logos/logo_expand.png")), "Expand All", self) - action_collapse = QAction(QIcon(join(cur_folder, "resources/logos/logo_collapse.png")), "Collapse All", self) - - action_expand.triggered.connect(self.tree_results.expandAll) - action_collapse.triggered.connect(self.tree_results.collapseAll) - - node_tree_context_menu.addActions([action_expand, action_collapse]) - - node_tree_context_menu.move(self.tree_results.mapToGlobal(position)) - node_tree_context_menu.exec_() - - def eventFilter(self, object_, event): - if event.type() == QEvent.HoverEnter: - self.label_description.setText(object_.property("description")) - self.label_image.set_scalable_pixmap(QPixmap(object_.property("image_path"))) - - return QWidget().eventFilter(object_, event) - - def clear_ui(self): - self.label_name.clear() - self.label_author.clear() - self.label_version.clear() - self.label_website.clear() - self.label_description.clear() - self.label_image.clear() - [widget.deleteLater() for widget in [ - self.layout_widget.itemAt(index).widget() for index in range(self.layout_widget.count()) - if self.layout_widget.itemAt(index).widget() - ]] - self.reset_models() - - def reset_models(self): - self.model_files.clear() - self.model_files.setHorizontalHeaderLabels(["Files Preview", "Source", "Plugin"]) - self.model_files_root = QStandardItem(QIcon(join(cur_folder, "resources/logos/logo_folder.png")), "") - self.model_files.appendRow(self.model_files_root) - self.tree_results.setModel(self.model_files) - self.model_flags.clear() - self.model_flags.setHorizontalHeaderLabels(["Flag Label", "Flag Value", "Plugin"]) - self.list_flags.setModel(self.model_flags) - - def clear_tab(self): - for index in reversed(range(self.mo_preview_layout.count())): - widget = self.mo_preview_layout.itemAt(index).widget() - if widget is not None: - widget.hide() - - def invalid_node(self): - self.clear_tab() - self.label_invalid.show() - - def missing_node(self): - self.clear_tab() - self.label_missing.show() - - def set_labels(self, name, author, version, website): - self.label_name.setText(name) - self.label_author.setText(author) - self.label_version.setText(version) - self.label_website.setText("link".format(website)) - - # this is pretty horrendous, need to come up with a better way of doing this. - def create_page(self, page_data): - group_step = QGroupBox(page_data.name) - layout_step = QVBoxLayout() - group_step.setLayout(layout_step) - - check_first_radio = True - for group in page_data.group_list: - group_group = QGroupBox(group.name) - layout_group = QVBoxLayout() - group_group.setLayout(layout_group) - - for plugin in group.plugin_list: - if group.type in ["SelectAny", "SelectAll", "SelectAtLeastOne"]: - button_plugin = QCheckBox(plugin.name, self) - - if group.type == "SelectAll": - button_plugin.setChecked(True) - button_plugin.setEnabled(False) - elif group.type == "SelectAtLeastOne": - button_plugin.toggled.connect( - lambda checked, button=button_plugin: button.setChecked(True) - if not checked and not [ - button for button in [ - layout_group.itemAt(index).widget() for index in range(layout_group.count()) - if layout_group.itemAt(index).widget() - ] if button.isChecked() - ] - else None - ) - - elif group.type in ["SelectExactlyOne", "SelectAtMostOne"]: - button_plugin = QRadioButton(plugin.name, self) - if check_first_radio and not button_plugin.isChecked(): - button_plugin.animateClick(0) - check_first_radio = False - - button_plugin.setProperty("description", plugin.description) - button_plugin.setProperty("image_path", plugin.image_path) - button_plugin.setProperty("file_list", plugin.file_list) - button_plugin.setProperty("folder_list", plugin.folder_list) - button_plugin.setProperty("flag_list", plugin.flag_list) - button_plugin.setProperty("type", plugin.type) - button_plugin.setAttribute(Qt.WA_Hover) - - if plugin.type == "Required": - button_plugin.setEnabled(False) - elif plugin.type == "Recommended": - button_plugin.animateClick(0) if not button_plugin.isChecked() else None - elif plugin.type == "NotUsable": - button_plugin.setChecked(False) - button_plugin.setEnabled(False) - - button_plugin.toggled.connect(self.reset_models) - button_plugin.toggled.connect(self.update_installed_files) - button_plugin.toggled.connect(self.update_set_flags) - - button_plugin.installEventFilter(self) - button_plugin.setObjectName("preview_button") - layout_group.addWidget(button_plugin) - - if group.type == "SelectAtMostOne": - button_none = QRadioButton("None") - layout_group.addWidget(button_none) - - layout_step.addWidget(group_group) - - self.layout_widget.addWidget(group_step) - self.reset_models() - self.update_installed_files() - self.update_set_flags() - self.show() - - def update_installed_files(self): - def recurse_add_items(folder, parent): - for boop in listdir(folder): # I was very tired - if isdir(join(folder, boop)): - folder_item = None - existing_folder_ = self.model_files.findItems(boop, Qt.MatchRecursive) - if existing_folder_: - for boopity in existing_folder_: - if boopity.parent() is parent: - folder_item = boopity - break - if not folder_item: - folder_item = self.PreviewItem( - QIcon(join(cur_folder, "resources/logos/logo_folder.png")), - boop - ) - folder_item.set_priority(folder_.priority) - parent.appendRow([folder_item, QStandardItem(rel_source), QStandardItem(button.text())]) - recurse_add_items(join(folder, boop), folder_item) - - elif isfile(join(folder, boop)): - file_item_ = None - existing_file_ = self.model_files.findItems(boop, Qt.MatchRecursive) - if existing_file_: - for boopity in existing_file_: - if boopity.parent() is parent: - if folder_.priority < boopity.priority: - file_item_ = boopity - break - else: - parent.removeRow(boopity.row()) - break - if not file_item_: - file_item_ = self.PreviewItem( - QIcon(join(cur_folder, "resources/logos/logo_file.png")), - boop - ) - file_item_.set_priority(folder_.priority) - parent.appendRow([file_item_, QStandardItem(rel_source), QStandardItem(button.text())]) - - for button in self.findChildren((QCheckBox, QRadioButton), "preview_button"): - for folder_ in button.property("folder_list"): - if (button.isChecked() and button.property("type") != "NotUsable" or - folder_.always_install or - folder_.install_usable and button.property("type") != "NotUsable" or - button.property("type") == "Required"): - destination = folder_.destination - abs_source = folder_.abs_source - rel_source = folder_.rel_source - parent_item = self.model_files_root - - destination_split = destination.split("/") - if destination_split[0] == ".": - destination_split = destination_split[1:] - for dest_folder in destination_split: - existing_folder_list = self.model_files.findItems(dest_folder, Qt.MatchRecursive) - if existing_folder_list: - for existing_folder in existing_folder_list: - if existing_folder.parent() is parent_item: - parent_item = existing_folder - break - continue - item_ = self.PreviewItem( - QIcon(join(cur_folder, "resources/logos/logo_folder.png")), - dest_folder - ) - item_.set_priority(folder_.priority) - parent_item.appendRow([item_, QStandardItem(), QStandardItem(button.text())]) - parent_item = item_ - - if isdir(abs_source): - recurse_add_items(abs_source, parent_item) - - for file_ in button.property("file_list"): - if (button.isChecked() and button.property("type") != "NotUsable" or - file_.always_install or - file_.install_usable and button.property("type") != "NotUsable" or - button.property("type") == "Required"): - destination = file_.destination - abs_source = file_.abs_source - rel_source = file_.rel_source - parent_item = self.model_files_root - - destination_split = destination.split("/") - if destination_split[0] == ".": - destination_split = destination_split[1:] - for dest_folder in destination_split: - existing_folder_list = self.model_files.findItems(dest_folder, Qt.MatchRecursive) - if existing_folder_list: - for existing_folder in existing_folder_list: - if existing_folder.parent() is parent_item: - parent_item = existing_folder - break - continue - item_ = self.PreviewItem( - QIcon(join(cur_folder, "resources/logos/logo_folder.png")), - dest_folder - ) - item_.set_priority(file_.priority) - parent_item.appendRow([item_, QStandardItem(), QStandardItem(button.text())]) - parent_item = item_ - - source_file = abs_source.split("/")[len(abs_source.split("/")) - 1] - file_item = None - existing_file_list = self.model_files.findItems(source_file, Qt.MatchRecursive) - if existing_file_list: - for existing_file in existing_file_list: - if existing_file.parent() is parent_item: - if file_.priority < existing_file.priority: - file_item = existing_file - break - else: - parent_item.removeRow(existing_file.row()) - break - if not file_item: - file_item = self.PreviewItem( - QIcon(join(cur_folder, "resources/logos/logo_file.png")), - source_file - ) - file_item.set_priority(file_.priority) - parent_item.appendRow([file_item, QStandardItem(rel_source), QStandardItem(button.text())]) - - self.tree_results.header().resizeSections(QHeaderView.Stretch) - - def update_set_flags(self): - for button in self.findChildren((QCheckBox, QRadioButton), "preview_button"): - if button.isChecked(): - for flag in button.property("flag_list"): - flag_label = QStandardItem(flag.label) - flag_value = QStandardItem(flag.value) - flag_plugin = QStandardItem(button.text()) - existing_flag = self.model_flags.findItems(flag.label) - if existing_flag: - previous_flag_row = existing_flag[0].row() - self.model_flags.removeRow(previous_flag_row) - self.model_flags.insertRow(previous_flag_row, [flag_label, flag_value, flag_plugin]) - else: - self.model_flags.appendRow([flag_label, flag_value, flag_plugin]) - - self.list_flags.header().resizeSections(QHeaderView.Stretch) diff --git a/dev/appveyor-bootstrap.bat b/dev/appveyor-bootstrap.bat index 4c4691c..58b58e7 100644 --- a/dev/appveyor-bootstrap.bat +++ b/dev/appveyor-bootstrap.bat @@ -1,12 +1,22 @@ @echo off -set PATH=C:\Miniconda-x64;C:\Miniconda-x64\Scripts; +C:\Miniconda-x64\Scripts\conda.exe create -y -n fomod-designer^ + -c m-labs^ + pyqt5=5.5.1 python=3.5.1 lxml=3.5.0 +call C:\Miniconda-x64\Scripts\activate.bat fomod-designer + +pip install pip -U +pip install setuptools -U --ignore-installed +pip install -r dev\reqs.txt +pip install -r dev\test-reqs.txt + -conda create -y -n fomod-designer^ - -c https://conda.anaconda.org/mmcauliffe^ +C:\Miniconda\Scripts\conda.exe create -y -n fomod-designer^ + -c m-labs^ pyqt5=5.5.1 python=3.5.1 lxml=3.5.0 -call activate fomod-designer +call C:\Miniconda\Scripts\activate.bat fomod-designer pip install pip -U pip install setuptools -U --ignore-installed pip install -r dev\reqs.txt +pip install -r dev\test-reqs.txt diff --git a/dev/pyinstaller-bootstrap.py b/dev/pyinstaller-bootstrap.py index 4060baa..7029bac 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 designer.__main__ import main # placed here so pycharm doesn't complain about import location + from src.__main__ import main # placed here so pycharm doesn't complain about import location main() diff --git a/dev/reqs.txt b/dev/reqs.txt index bbfc2f1..be78f7c 100644 --- a/dev/reqs.txt +++ b/dev/reqs.txt @@ -1,8 +1,21 @@ +alabaster==0.7.8 +Babel==2.3.4 bumpversion==0.5.3 +coverage==4.1 fomod-validator==1.5.3 +docutils==0.12 +imagesize==0.7.1 invoke==0.12.2 +Jinja2==2.8 jsonpickle==0.9.3 lxml==3.5.0 +py==1.4.31 +MarkupSafe==0.23 Pygments==2.1.3 PyInstaller==3.1.1 +pytz==2016.4 requests==2.10.0 +six==1.10.0 +snowballstemmer==1.2.1 +Sphinx==1.4.3 +sphinx-rtd-theme==0.1.9 diff --git a/dev/test-reqs.txt b/dev/test-reqs.txt new file mode 100644 index 0000000..1a1ae9c --- /dev/null +++ b/dev/test-reqs.txt @@ -0,0 +1,4 @@ +pytest==2.9.2 +pytest-cov==2.3.1 +pytest-qt==2.0.0 +coveralls==1.1 diff --git a/dev/travis-bootstrap.sh b/dev/travis-bootstrap.sh index ce27042..2fb9648 100644 --- a/dev/travis-bootstrap.sh +++ b/dev/travis-bootstrap.sh @@ -51,3 +51,4 @@ pyenv shell miniconda3-3.19.0/envs/fomod-designer pip install pip -U pip install setuptools -U --ignore-installed pip install -r dev/reqs.txt +pip install -r dev/test-reqs.txt diff --git a/dev/travis-test.sh b/dev/travis-test.sh new file mode 100644 index 0000000..05f1f0e --- /dev/null +++ b/dev/travis-test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# 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. + +export PYENV_ROOT="$HOME/.pyenv-custom" +export PATH="$PYENV_ROOT/bin:$PATH" +eval "$(pyenv init -)" +eval "$(pyenv virtualenv-init -)" + +pyenv shell miniconda3-3.19.0/envs/fomod-designer + +py.test --cov=src --cov-report html --cov-report term -vv tests/ + +coveralls diff --git a/dev/vagrant-bootstrap.sh b/dev/vagrant-bootstrap.sh index d3905f2..7e57c2f 100644 --- a/dev/vagrant-bootstrap.sh +++ b/dev/vagrant-bootstrap.sh @@ -18,7 +18,6 @@ sudo apt-get update # fix locale issues - { echo 'export LANGUAGE=en_US.UTF-8' echo 'export LANG=en_US.UTF-8' @@ -31,14 +30,12 @@ sudo dpkg-reconfigure locales # get git - needed for pyenv - sudo apt-get install -y git git-flow sudo apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \ libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev # shorten the command prompt - echo 'parse_git_branch() { git branch 2> /dev/null | sed -e '\''/^[^*]/d'\'' -e '\''s/* \(.*\)/ (\1)/'\'' } @@ -50,8 +47,14 @@ parse_git_branch() { export PS1="\[\033[38;5;10m\]\u@ \$(parse_git_branch)\w\\$ \[$(tput sgr0)\]" -# configure git so you don't have to go back and forward all the time. +# install p4merge +wget http://cdist2.perforce.com/perforce/r15.2/bin.linux26x86_64/p4v.tgz +tar zxvf p4v.tgz +sudo mv p4v-* /opt/p4v +sudo ln -s /opt/p4v/bin/p4merge /usr/local/bin/p4merge + +# configure git so you don't have to go back and forward all the time. python3 - <> /home/vagrant/.bashrc @@ -102,12 +118,10 @@ eval "$(pyenv virtualenv-init -)" # start installing the python versions - 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-designer \ -c https://conda.anaconda.org/mmcauliffe \ @@ -117,7 +131,6 @@ pyenv shell miniconda3-3.19.0/envs/fomod-designer # move to the project folder and install the pip reqs - cd /vagrant || exit pip install pip -U pip install setuptools -U --ignore-installed diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..a7632dc --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,225 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FOMODDesigner.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FOMODDesigner.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/FOMODDesigner" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FOMODDesigner" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..0b2706e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,281 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. epub3 to make an epub3 + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + echo. dummy to check syntax errors of document sources + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\FOMODDesigner.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\FOMODDesigner.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "epub3" ( + %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +if "%1" == "dummy" ( + %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. Dummy builder generates no files. + goto end +) + +:end diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst new file mode 100644 index 0000000..15c2596 --- /dev/null +++ b/docs/source/advanced.rst @@ -0,0 +1,1177 @@ +Advanced Usage +============== + +For the advanced users and everyone who knows their way around a *FOMOD* installer. +In this section you'll find descriptions of the tags and nodes themselves - what they are, how to use them and +examples when needed. + +There are no restrictions when using the *Advanced View*, we trust that you know what you're doing. +This is recommended for people who already know how to create/modify XML +installers and are interested in speeding up their work or for users who want more customization options than the +*Basic View* offers. + +Advanced View ++++++++++++++ + +The *Advanced View* can be divided in 4 parts: **Node Tree**, **Previews**, **Property Editor** and **Children Box**. +All of these, with the exception of the **Previews**, can be moved around by the user. + +The **Node Tree**, by default situated on the left, contains all the nodes in the installer's two trees: the `Info`_ and +the `Config`_ tree. You can right-click the tree to see all the actions available - some of these, like *Delete*, are +not available for the root nodes. You can also traverse the tree with the arrow keys and use the Enter key or left-click +to select the node, this will update the **Property Editor** and the **Children Box** (and the **Previews** in case that +is enabled). + +The **Previews**, situated on the center, has two tabs: *GUI Preview* and *XML Preview*. The *GUI Preview* has a Mod +Organizer-like interface that simulates the current `Install Step`_ - you can choose the options and the bottom half +reflects the flags that would be set and/or the files that would be installed. The *XML Preview* has a preview of the +XML code that that node and its children would output. + +The **Property Editor**, by default situated on the top right, contains all the editable properties for the currently +selected node. You can find more information for each node's properties in the `FOMOD Bible`_. + +The **Children Box**, by default situated on the bottom right, contains all the available children to add to the +currently selected node. Click on a child button here to add the corresponding node. + +Learn you a FOMOD For Great Good +++++++++++++++++++++++++++++++++ + +This section contains the `FOMOD Bible`_ - a description of all the tags/nodes with examples. +This is not meant to be read from top to bottom but rather as a dictionary or a glossary - +search for the tag/node you need more info on with the search box on the left sidebar. + +Tag vs Node +........... + +A **Tag** is any item within an xml document. Within the *FOMOD* schema +(the document that defines the rules for installer documents) +all the allowed tags for FOMOD are defined. A tag has the format ```` or +``text goes here`` if it contains text. + +Similarly, any item in the *FOMOD Designer*'s *Node Tree* is a **Node**. +Every node has a direct correspondence to a xml tag. +These two terms are use interchangeably in the *FOMOD Bible*. + +Attribute vs Property +..................... + +An **Attribute** is a way to customize a tag. These are also defined in the *FOMOD* schema and have the format: +````. + +A **Property** is the attribute equivalent for nodes. They can be edited via the *Property Editor*. In the *Bible* +the properties are always followed by the corresponding attribute in square brackets (p.e. Name [name]). + +.. attention:: + While a tag's text (p.e. ``text goes here``) is not an attribute, in a node it is bundled together + with its properties for convenience. In order to distinguish text from other properties, it is marked with + [...] as its attribute. + + So for a node that can have text its properties will have the line: + Text [...] + +Tag Order +......... + +Some tags are enforced a specific order by the *FOMOD* schema. +When applicable, the possible/required children listed in each node are ordered. + +This enforced order is reflected in the node tree. The user is able to modify the order +of repeatable nodes through drag and drop. + +FOMOD Bible +........... + +Please take note this isn't a fully comprehensive document (at least so far). If you want something more complete, +feel free to look at the `revised *FOMOD* schema `_. + +Info +---- + +Tag + fomod + +Description + The root node for the document containing all the information relative to the installer. + +Children + ====================== ========== + Node Repeatable + ====================== ========== + :ref:`info_name_label` **No** + `Author`_ **No** + :ref:`info_desc_label` **No** + `ID`_ **No** + `Categories Group`_ **No** + `Version`_ **No** + `Website`_ **No** + ====================== ========== + +Properties + *None* + +------------------------------------- + +.. _info_name_label: + +Name +---- + +Tag + Name + +Description + The node that holds the mod's name. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Text [...] The name of the mod. + =============== =============== =============================== + +------------------------------------- + +Author +------ + +Tag + Author + +Description + The node that holds the mod's author(s). + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Text [...] The author(s) of the mod. + =============== =============== =============================== + +------------------------------------- + +.. _info_desc_label: + +Description +----------- + +Tag + Description + +Description + The node that holds the mod's description. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Text [...] The description of the mod. + =============== =============== =============================== + +------------------------------------- + +ID +-- + +Tag + Id + +Description + The node that holds the mod's ID. + The ID is the last part of the nexus' link. Example: + + Nexus mod link: http://www.nexusmods.com/skyrim/mods/548961 -> ID's text is 548961 + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Text [...] The ID of the mod. + =============== =============== =============================== + +------------------------------------- + +Categories Group +---------------- + +Tag + Groups + +Description + This node's purpose is solely to group the categories this mod belongs to together. + +Children + ====================== ========== + Node Repeatable + ====================== ========== + `Category`_ **Yes** + ====================== ========== + +Properties + *None* + +------------------------------------- + +Category +-------- + +Tag + element + +Description + The node that holds one of the mod's category. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Text [...] A category this mod belongs to. + =============== =============== =============================== + +------------------------------------- + +Version +------- + +Tag + Version + +Description + The node that holds the mod's version. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Text [...] This mod's version. + =============== =============== =============================== + +------------------------------------- + +Website +------- + +Tag + Website + +Description + The node that holds the mod's home website. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Text [...] The mod's home website. + =============== =============== =============================== + +------------------------------------- + +Config +------ + +Tag + config + +Description + The main element containing the module configuration info. + +Children + =========================== ========== ================================================ + Node Repeatable Notes + =========================== ========== ================================================ + :ref:`config_name_label` **No** + :ref:`mod_image_label` **No** + `Mod Dependencies`_ **No** At least one of the following is required + for the installer to have any effect: + `Mod Dependencies`_, `Installation Steps`_, + `Mod Requirements`_, `Conditional Installation`_ + `Installation Steps`_ **No** At least one of the following is required + for the installer to have any effect: + `Mod Dependencies`_, `Installation Steps`_, + `Mod Requirements`_, `Conditional Installation`_ + `Mod Requirements`_ **No** At least one of the following is required + for the installer to have any effect: + `Mod Dependencies`_, `Installation Steps`_, + `Mod Requirements`_, `Conditional Installation`_ + `Conditional Installation`_ **No** At least one of the following is required + for the installer to have any effect: + `Mod Dependencies`_, `Installation Steps`_, + `Mod Requirements`_, `Conditional Installation`_ + =========================== ========== ================================================ + +Properties + =============== ==================================================================== =============================== + Property Attribute Description + =============== ==================================================================== =============================== + *N/A* {http://www.w3.org/2001/XMLSchema-instance}noNamespaceSchemaLocation This attribute contains the + namespace for this file. + + This property is not editable. + + The value should always be: + ``"http://qconsulting.ca/fo3/ModConfig5.0.xsd"`` + =============== ==================================================================== =============================== + +------------------------------------- + +.. _config_name_label: + +Name +---- + +Tag + moduleName + +Description + The name of the module. Used to describe the display properties of the module title. + +Children + *None* + +Properties + =============== =============== ================================================================= + Property Attribute Description + =============== =============== ================================================================= + Text [...] The name of the mod. + Position position The position of the mod's name in the header. + + Accepts the values: ``"Left"``, ``"Right"`` or ``"RightOfImage"`` + Colour colour The colour of the mod's name in the header. + + Accepts RGB hex values. + =============== =============== ================================================================= + +------------------------------------- + +.. _mod_image_label: + +Image +----- + +Tag + moduleImage + +Description + The module logo/banner. + + *[Ignored in Mod Organizer]* + +Children + *None* + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Path path The path to the image file. + Show Image showImage Whether the image is visible. + + Accepts ``true`` or ``false`` + Show Fade showFade Whether the image's opacity is fixed. + + Accepts ``true`` or ``false`` + Height height The maximum height of the image. + + Accepts any integer larger than ``-1`` + =============== =============== ====================================== + +------------------------------------- + +Mod Dependencies +---------------- + +Tag + moduleDependencies + +Description + Items upon which the module depends. The installation process will only start after these conditions have been met. + + While flag dependencies are allowed they should not be used since no flag will have been set at the time these + conditions are checked. + +Children + =========================== ========== + Node Repeatable + =========================== ========== + `File Dependency`_ **Yes** + `Flag Dependency`_ **Yes** + `Game Dependency`_ **No** + `Dependencies`_ **Yes** + =========================== ========== + +Properties + =============== =============== ============================================= + Property Attribute Description + =============== =============== ============================================= + Type operator The type of the dependency: ``And`` or ``Or`` + + If the type is ``And``, all conditions under + this node must be met. + + If the type is ``Or``, only one condition + must be met. + =============== =============== ============================================= + +------------------------------------- + +File Dependency +--------------- + +Tag + fileDependency + +Description + Specifies that a mod must be in a specified state. + +Children + *None* + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + File file The path to the file to be checked. + State state The supposed state of the file. + =============== =============== ====================================== + +------------------------------------- + +Flag Dependency +--------------- + +Tag + flagDependency + +Description + Specifies that a condition flag must have a specific value. + +Children + *None* + +Properties + =============== =============== ========================================= + Property Attribute Description + =============== =============== ========================================= + Flag flag The flag where this condition falls upon. + Value value The value of the flag to be checked. + =============== =============== ========================================= + +------------------------------------- + +Game Dependency +--------------- + +Tag + gameDependency + +Description + Specifies a minimum required version of the installed game. + + *[Ignored in Mod Organizer]* + +Children + *None* + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Version version The minimum version of the game. + =============== =============== ====================================== + +------------------------------------- + +Installation Steps +------------------ + +Tag + installSteps + +Description + The list of install steps that determine which files (or plugins) that may optionally be installed for this module. + +Children + =========================== ========== ============================================ + Node Repeatable Notes + =========================== ========== ============================================ + `Install Step`_ **Yes** At least one of `Install Step`_ is required. + =========================== ========== ============================================ + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Order order The order of the install steps beneath + this node. + ``"Explicit"`` follows document + order while the others order + alphabetically. + + Accepts ``"Ascending"``, + ``"Descending"`` or ``"Explicit"`` + =============== =============== ====================================== + +------------------------------------- + +Install Step +------------ + +Tag + installStep + +Description + A step in the install process containing groups of optional plugins. + +Children + =========================== ========== ============================================ + Node Repeatable Notes + =========================== ========== ============================================ + `Visibility`_ **No** + `Option Group`_ **No** At least one of `Option Group`_ is required. + =========================== ========== ============================================ + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Name name The name of this install step. + =============== =============== ====================================== + +------------------------------------- + +Visibility +---------- + +Tag + visible + +Description + The pattern against which to match the conditional flags and installed files. + If the pattern is matched, then the install step will be visible. + +Children + =========================== ========== + Node Repeatable + =========================== ========== + `File Dependency`_ **Yes** + `Flag Dependency`_ **Yes** + `Game Dependency`_ **No** + `Dependencies`_ **Yes** + =========================== ========== + +Properties + =============== =============== ============================================= + Property Attribute Description + =============== =============== ============================================= + Type operator The type of the dependency: ``And`` or ``Or`` + + If the type is ``And``, all conditions under + this node must be met. + + If the type is ``Or``, only one condition + must be met. + =============== =============== ============================================= + +------------------------------------- + +Dependencies +------------ + +Tag + dependencies + +Description + A dependency that is made up of one or more dependencies. + +Children + =========================== ========== + Node Repeatable + =========================== ========== + `File Dependency`_ **Yes** + `Flag Dependency`_ **Yes** + `Game Dependency`_ **No** + `Dependencies`_ **Yes** + =========================== ========== + +Properties + =============== =============== ============================================= + Property Attribute Description + =============== =============== ============================================= + Type operator The type of the dependency: ``And`` or ``Or`` + + If the type is ``And``, all conditions under + this node must be met. + + If the type is ``Or``, only one condition + must be met. + =============== =============== ============================================= + +------------------------------------- + +Option Group +------------ + +Tag + optionalFileGroups + +Description + The list of optional files (or plugins) that may optionally be installed for this module. + +Children + =========================== ========== ===================================== + Node Repeatable Notes + =========================== ========== ===================================== + `Group`_ **Yes** At least one of `Group`_ is required. + =========================== ========== ===================================== + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Order order The order of the install steps beneath + this node. + ``"Explicit"`` follows document + order while the others order + alphabetically. + + Accepts ``"Ascending"``, + ``"Descending"`` or ``"Explicit"`` + =============== =============== ====================================== + +------------------------------------- + +Group +----- + +Tag + group + +Description + A group of plugins for the mod. + +Children + =========================== ========== ======================================= + Node Repeatable Notes + =========================== ========== ======================================= + `Plugins`_ **No** At least one of `Plugins`_ is required. + =========================== ========== ======================================= + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Name name The name of this group. + Type type The selection type for this group. + + Accepts ``"SelectAny"``, + ``"SelectAtMostOne"``, + ``"SelectExactlyOne"``, + ``"SelectAll"`` or + ``"SelectAtLeastOne"`` + =============== =============== ====================================== + +------------------------------------- + +Plugins +------- + +Tag + plugins + +Description + The list of plugins in the group. + +Children + =========================== ========== ====================================== + Node Repeatable Notes + =========================== ========== ====================================== + `Plugin`_ **Yes** At least one of `Plugin`_ is required. + =========================== ========== ====================================== + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Order order The order of the plugins beneath + this node. + ``"Explicit"`` follows document + order while the others order + alphabetically. + + Accepts ``"Ascending"``, + ``"Descending"`` or ``"Explicit"`` + =============== =============== ====================================== + +------------------------------------- + +Plugin +------ + +Tag + plugin + +Description + A mod plugin belonging to a group. + +Children + =========================== ========== ===================================================== + Node Repeatable Notes + =========================== ========== ===================================================== + :ref:`config_desc_label` **No** At least one of :ref:`config_desc_label` is required. + :ref:`plugin_image_label` **No** + `Files`_ **No** + `Flags`_ **No** + `Type Descriptor`_ **No** At least one of `Type Descriptor`_ is required. + =========================== ========== ===================================================== + +Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Name name The name of this plugin. + =============== =============== ====================================== + +------------------------------------- + +.. _config_desc_label: + +Description +----------- + +Tag + description + +Description + A description of the plugin. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Description [...] The plugin's description. + =============== =============== =============================== + +------------------------------------- + +.. _plugin_image_label: + +Image +----- + +Tag + image + +Description + The optional image associated with a plugin. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Path path The path to the image. + =============== =============== =============================== + +------------------------------------- + +Files +----- + +Tag + files + +Description + A list of files and folders to be installed. + +Children + =========================== ========== + Node Repeatable + =========================== ========== + `File`_ **Yes** + `Folder`_ **Yes** + =========================== ========== + +Properties + *None* + +------------------------------------- + +File +---- + +Tag + file + +Description + A file belonging to the plugin or module. + +Children + *None* + +Properties + ================= =============== =================================== + Property Attribute Description + ================= =============== =================================== + Source source The path to the file. + Destination destination The path from the game's mod folder + to the destination of this file. + Priority priority The priority of the file. + + Higher priority means the file will + overwrite other files with lower + priority. + Always Install alwaysInstall If ``true``, this file will be + always installed, regardless of the + user's choice. + + Accepts ``true`` or ``false`` + Install If Usable installIfUsable If ``true``, this file will be + installed unless the plugin's type + is ``NotUsable``, regardless of the + user's choice. + + Accepts ``true`` or ``false`` + ================= =============== =================================== + +------------------------------------- + +Folder +------ + +Tag + folder + +Description + A folder belonging to the plugin or module. + +Children + *None* + +Properties + ================= =============== =================================== + Property Attribute Description + ================= =============== =================================== + Source source The path to the folder. + Destination destination The path from the game's mod folder + to the destination of this folder. + Priority priority The priority of the folder. + + Higher priority means the folder + will + overwrite other files with lower + priority. + Always Install alwaysInstall If ``true``, this folder will be + always installed, regardless of the + user's choice. + + Accepts ``true`` or ``false`` + Install If Usable installIfUsable If ``true``, this folder will be + installed unless the plugin's type + is ``NotUsable``, regardless of the + user's choice. + + Accepts ``true`` or ``false`` + ================= =============== =================================== + +------------------------------------- + +Flags +----- + +Tag + conditionFlags + +Description + The list of condition flags to set if the plugin is in the appropriate state. + +Children + =========================== ========== ==================================== + Node Repeatable Notes + =========================== ========== ==================================== + `Flag`_ **Yes** At least one of `Flag`_ is required. + =========================== ========== ==================================== + +Properties + *None* + +------------------------------------- + +Flag +---- + +Tag + flag + +Description + A condition flag to set if the plugin is selected. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Label name The flag's identifying label. + Value [...] The flag's new value. + =============== =============== =============================== + +------------------------------------- + +Type Descriptor +--------------- + +Tag + typeDescriptor + +Description + Describes the type of a plugin. + +Children + =========================== ========== ================================================== + Node Repeatable Notes + =========================== ========== ================================================== + `Dependency Type`_ **No** Either `Dependency Type`_ or `Type`_ must be used. + `Type`_ **No** Either `Dependency Type`_ or `Type`_ must be used. + =========================== ========== ================================================== + +Properties + *None* + +------------------------------------- + +Dependency Type +--------------- + +Tag + dependencyType + +Description + Used when the plugin type is dependent upon the state of other mods. + +Children + =========================== ========== =================================================== + Node Repeatable Notes + =========================== ========== =================================================== + :ref:`depend_patterns` **No** At least one of :ref:`depend_patterns` is required. + `Default Type`_ **No** At least one of `Default Type`_ is required. + =========================== ========== =================================================== + +Properties + *None* + +------------------------------------- + +.. _depend_patterns: + +Patterns +-------- + +Tag + patterns + +Description + The list of dependency patterns against which to match the user's installation. + The first pattern that matches the user's installation determines the type of the plugin. + +Children + =========================== ========== ================================================== + Node Repeatable Notes + =========================== ========== ================================================== + :ref:`depend_pattern` **Yes** At least one of :ref:`depend_pattern` is required. + =========================== ========== ================================================== + +Properties + *None* + +------------------------------------- + +.. _depend_pattern: + +Pattern +------- + +Tag + pattern + +Description + A specific pattern of mod files and condition flags against which to match the user's installation. + +Children + =========================== ========== ============================================ + Node Repeatable Notes + =========================== ========== ============================================ + `Dependencies`_ **No** At least one of `Dependencies`_ is required. + `Type`_ **No** At least one of `Type`_ is required. + =========================== ========== ============================================ + +Properties + *None* + +------------------------------------- + +Type +---- + +Tag + type + +Description + The type of the plugin. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Type name Describes the plugin's type. + + Accepts ``Required``, + ``Recommended``, ``Optional``, + ``CouldBeUsable`` or + ``NotUsable`` + =============== =============== =============================== + +------------------------------------- + +Default Type +------------ + +Tag + defaultType + +Description + The default type of the plugin used if none of the specified dependency states are satisfied. + +Children + *None* + +Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Type name Describes the plugin's type. + + Accepts ``Required``, + ``Recommended``, ``Optional``, + ``CouldBeUsable`` or + ``NotUsable`` + =============== =============== =============================== + +------------------------------------- + +Mod Requirements +---------------- + +Tag + requiredInstallFiles + +Description + The list of files and folders that must be installed for this module. + +Children + =========================== ========== + Node Repeatable + =========================== ========== + `File`_ **Yes** + `Folder`_ **Yes** + =========================== ========== + +Properties + *None* + +------------------------------------- + +Conditional Installation +------------------------ + +Tag + conditionalFileInstalls + +Description + The list of optional files that may optionally be installed for this module, based on condition flags. + +Children + =========================== ========== ================================================= + Node Repeatable Notes + =========================== ========== ================================================= + :ref:`cond_patterns` **No** At least one of :ref:`cond_patterns` is required. + =========================== ========== ================================================= + +Properties + *None* + +------------------------------------- + +.. _cond_patterns: + +Patterns +-------- + +Tag + patterns + +Description + The list of patterns against which to match the conditional flags and installed files. + All matching patterns will have their files installed. + +Children + =========================== ========== ================================================ + Node Repeatable Notes + =========================== ========== ================================================ + :ref:`cond_pattern` **Yes** At least one of :ref:`cond_pattern` is required. + =========================== ========== ================================================ + +Properties + *None* + +------------------------------------- + +.. _cond_pattern: + +Pattern +------- + +Tag + pattern + +Description + A specific pattern of mod files and condition flags against which to match the user's installation. + +Children + =========================== ========== ============================================ + Node Repeatable Notes + =========================== ========== ============================================ + `Files`_ **No** At least one of `Files`_ is required. + `Dependencies`_ **No** At least one of `Dependencies`_ is required. + =========================== ========== ============================================ + +Properties + *None* diff --git a/docs/source/basic.rst b/docs/source/basic.rst new file mode 100644 index 0000000..db33637 --- /dev/null +++ b/docs/source/basic.rst @@ -0,0 +1,25 @@ +Basic Usage +=========== + +.. todo:: + Describe basic usage - basic view and wizards. + +For first-time users and those who don't really want to think too much about it. +Follow each wizard's instructions in the app to fully build an installer. +Remember than you can only save or open a new installer when on the very first page! +If you're mid-way through your work but you want to save and leave, simply hit ``Finish`` until you reach that first page. + +Basic View +++++++++++ + +.. todo:: + Describe basic view here - pretty much just the initial page. + +Wizards ++++++++ + +This section contains the descriptions of all the wizards so if you have any doubts simply come check here! +To search for a specific wizard use the search box on the left sidebar. + +.. todo:: + Finish wizards, not sure what to write here though. diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 0000000..09929fe --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1 @@ +.. include:: ../../CHANGELOG.rst diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..52eb5ed --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# FOMOD Designer documentation build configuration file, created by +# sphinx-quickstart on Thu Jun 9 19:41:15 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +from configparser import ConfigParser +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'FOMOD Designer' +copyright = '2016, Daniel Nunes' +author = 'Daniel Nunes' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +try: + build_number = os.environ["APPVEYOR_BUILD_NUMBER"] +except KeyError: + try: + build_number = os.environ["TRAVIS_BUILD_NUMBER"] + except KeyError: + build_number = 0 + +config = ConfigParser() +config.read(os.path.join(os.path.dirname(__file__), "..", "..", "setup.cfg")) + +version = release = config.get('bumpversion', 'current_version') + "." + str(build_number) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + + +# -- Options for HTML output ---------------------------------------------- + +# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' + +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = 'FOMOD Designer v0.4.1' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'FOMODDesignerdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'FOMODDesigner.tex', 'FOMOD Designer Documentation', + 'Daniel Nunes', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'fomoddesigner', 'FOMOD Designer Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'FOMODDesigner', 'FOMOD Designer Documentation', + author, 'FOMODDesigner', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst new file mode 100644 index 0000000..ac7b6bc --- /dev/null +++ b/docs/source/contributing.rst @@ -0,0 +1 @@ +.. include:: ../../CONTRIBUTING.rst diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 0000000..8ddc0d1 --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,31 @@ +Frequently Asked Questions +========================== + +.. _open_new_merged: + +Why are the *New* and *Open* buttons merged? +++++++++++++++++++++++++++++++++++++++++++++ + +Ok, let's run through what would happen in the code for the **New** button: + + 1. Get the package folder from the user; + + 2. Check if an installer exists in that folder; + + 3. If it doesn't exist, create a new one; + + 4. It it does exist, complain to the user. + +Now for the **Open** button: + + 1. Get the package folder from the user; + + 2. Check if an installer exists in that folder; + + 3. If it doesn't exist, complain to the user; + + 4. It it does exist, open it up. + +Do you see how similar these two are? It wouldn't really make sense to have +two completely separate actions that do pretty much the same thing. This way +everything is much simpler on our side and we never have to complain to you! diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..c6ee98a --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,38 @@ +Getting Started +=============== + +Welcome to the *FOMOD Designer* documentation! Let's get right to it. + +Run the executable that comes with the package. If you need help with getting the correct package for you see +the :doc:`Installation ` page. + +First, you'll see the **Intro** window. At the bottom of this window you'll see your most recently opened installers, +in the future you can select one here to open it more quickly. Since you most likely have no recent installers, +click the ``New/Open`` button. Here you'll choose the folder where the package you want to make an installer for +is located. + +.. note:: + Now for an important distinction from other apps you may have used: the *FOMOD Designer* does not have separate + **New** and **Open** buttons. Simply select the correct folder and it'll auto detect an existing installer. + If you want to know about the behind the scenes for this, check the :ref:`F.A.Q. `. + +The **Main** window should now appear. If you're a first-time user it should load the *Basic View* and you should head +on to the :doc:`Basic Usage `. In case you're a returning user and/or you've enabled the *Advanced View* +head to the :doc:`Advanced Usage `. + +.. attention:: + If you need help with a button or something else on the window, try hovering over it and checking the bottom left + of the screen, in the status bar. + +.. toctree:: + :caption: Contents: + :hidden: + :maxdepth: 1 + + Getting Started + Installation + Basic Usage + Advanced Usage + Contributing + Changelog + F.A.Q. diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 0000000..5c9cbe3 --- /dev/null +++ b/docs/source/install.rst @@ -0,0 +1,96 @@ +Installation +============ + +**TL;DR:** All you need to do is `download the package `_, +extract it somewhere and run the ``FOMOD Designer`` executable. + +Pre-Built Executables ++++++++++++++++++++++ + +There are pre-built, ready-to-use executables always available for +64-bit Windows and often for 64-bit Linux as well. + +It is recommended to use the `latest stable version `_ +since it's less likely to have critical bugs. If you need to use a feature that +hasn't made it to the stable builds, feel free to download the `bleeding edge build `_. + +If there are no builds for your system or you just love to have tons +of work try building from source. + +Building from Source +++++++++++++++++++++ + +1. Download the `repository from Github `_; + +2. Unpack the archive into a folder; + +3. Install `Conda `_; + +4. Open the command line/terminal in the folder from step 2; + +5. Create the necessary environment within Conda: + + * Windows 64-bit: + + .. code-block:: batch + + conda create -y -n fomod-designer^ + -c https://conda.anaconda.org/mmcauliffe^ + pyqt5=5.5.1 python=3.5.1 lxml=3.5.0 + + * Linux 64-bit: + + .. code-block:: shell + + conda create -y -n fomod-designer \ + -c https://conda.anaconda.org/mmcauliffe \ + pyqt5=5.5.1 python=3.5.1 lxml=3.5.0 + + * For other platforms you'll have to figure out where the correct Conda packages are. As of now, you'll need these: + + ======= ======= + Package Version + ======= ======= + PyQt5 5.5.1 + lxml 3.5.0 + ======= ======= + +6. Activate the environment: + + * Windows: + + .. code-block:: batch + + activate fomod-designer + + * Other: + + .. code-block:: shell + + source activate fomod-designer + +7. Install other dependencies: + + * Windows: + + .. code-block:: batch + + pip install pip -U + pip install setuptools -U --ignore-installed + pip install -r dev\reqs.txt + + * Other: + + .. code-block:: shell + + pip install pip -U + pip install setuptools -U --ignore-installed + pip install -r dev/reqs.txt + +8. Build the app: + + .. code-block:: shell + + inv build + +9. Done! The built package is in the ``dist`` folder within the folder in step 2. diff --git a/resources/logos/logo_hide.png b/resources/logos/logo_hide.png new file mode 100644 index 0000000..d1dd238 Binary files /dev/null and b/resources/logos/logo_hide.png differ diff --git a/resources/logos/logo_show.png b/resources/logos/logo_show.png new file mode 100644 index 0000000..e859fce Binary files /dev/null and b/resources/logos/logo_show.png differ diff --git a/resources/templates/window_intro.ui b/resources/templates/window_intro.ui index 34cd7a5..0f514a6 100644 --- a/resources/templates/window_intro.ui +++ b/resources/templates/window_intro.ui @@ -135,6 +135,12 @@ 0 + + + 125 + 0 + + false @@ -158,6 +164,49 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 125 + 0 + + + + Getting Started + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -175,6 +224,12 @@ + + + 125 + 0 + + About diff --git a/resources/templates/window_mainframe.ui b/resources/templates/window_mainframe.ui index 46aa641..2644500 100644 --- a/resources/templates/window_mainframe.ui +++ b/resources/templates/window_mainframe.ui @@ -308,9 +308,6 @@ &Tools - - - @@ -616,9 +613,6 @@ Expand All - - Ctrl+* - @@ -628,8 +622,35 @@ Collapse All + + + + + ../logos/logo_hide.png../logos/logo_hide.png + + + Hide Node + + + Hide Node + + + Ctrl+X + + + + + + ../logos/logo_show.png../logos/logo_show.png + + + Show Node + + + Show Node + - Ctrl+. + Ctrl+X diff --git a/setup.cfg b/setup.cfg index bb95662..3d4acfb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,10 @@ [bumpversion] -current_version = 0.7.2 +current_version = 0.8.0 current_build = 0 [bdist_wheel] -universal = 1 +universal = 0 + +[coverage:run] +omit = src/ui_templates/* diff --git a/designer/__init__.py b/src/__init__.py similarity index 100% rename from designer/__init__.py rename to src/__init__.py diff --git a/designer/__main__.py b/src/__main__.py similarity index 100% rename from designer/__main__.py rename to src/__main__.py diff --git a/designer/exceptions.py b/src/exceptions.py similarity index 100% rename from designer/exceptions.py rename to src/exceptions.py diff --git a/designer/gui.py b/src/gui.py similarity index 75% rename from designer/gui.py rename to src/gui.py index a7105fd..5743be9 100644 --- a/designer/gui.py +++ b/src/gui.py @@ -14,33 +14,35 @@ # See the License for the specific language governing permissions and # limitations under the License. -from os import makedirs -from os.path import expanduser, normpath, basename, join, relpath, isdir +from os import makedirs, listdir +from os.path import expanduser, normpath, basename, join, relpath, isdir, isfile, abspath from io import BytesIO from threading import Thread from queue import Queue -from webbrowser import open as web_open +from webbrowser import open_new_tab from datetime import datetime from collections import deque from json import JSONDecodeError from jsonpickle import encode, decode, set_encoder_options -from lxml.etree import parse, tostring +from lxml.etree import parse, tostring, Comment from PyQt5.QtWidgets import (QFileDialog, QColorDialog, QMessageBox, QLabel, QHBoxLayout, QCommandLinkButton, QDialog, QFormLayout, QLineEdit, QSpinBox, QComboBox, QWidget, QPushButton, QSizePolicy, QStatusBar, - QCompleter, QApplication, QMainWindow, QUndoCommand, QUndoStack, QMenu) -from PyQt5.QtGui import QIcon, QPixmap, QColor, QFont, QStandardItemModel -from PyQt5.QtCore import Qt, pyqtSignal, QStringListModel, QMimeData + QCompleter, QApplication, QMainWindow, QUndoCommand, QUndoStack, QMenu, QHeaderView, + QAction, QVBoxLayout, QGroupBox, QCheckBox, QRadioButton) +from PyQt5.QtGui import QIcon, QPixmap, QColor, QFont, QStandardItemModel, QStandardItem +from PyQt5.QtCore import Qt, pyqtSignal, QStringListModel, QMimeData, QEvent from PyQt5.uic import loadUi -from requests import get, codes, ConnectionError, Timeout +from requests import get, head, codes, ConnectionError, Timeout from validator import validate_tree, check_warnings, ValidatorError, ValidationError, WarningError, MissingFolderError from . import cur_folder, __version__ -from .io import import_, new, export, sort_elements, elem_factory, copy_element -from .previews import PreviewDispatcherThread, PreviewMoGui +from .nodes import _NodeElement, NodeComment +from .io import import_, new, export, node_factory, copy_node +from .previews import PreviewDispatcherThread from .props import PropertyFile, PropertyColour, PropertyFolder, PropertyCombo, PropertyInt, PropertyText, \ PropertyFlagLabel, PropertyFlagValue, PropertyHTML from .exceptions import DesignerError from .ui_templates import window_intro, window_mainframe, window_about, window_settings, window_texteditor, \ - window_plaintexteditor + window_plaintexteditor, preview_mo class IntroWindow(QMainWindow, window_intro.Ui_MainWindow): @@ -74,6 +76,7 @@ def __init__(self): self.show() self.new_button.clicked.connect(lambda: self.open_path("")) + self.button_help.clicked.connect(MainFrame.help) self.button_about.clicked.connect(lambda _, self_=self: MainFrame.about(self_)) def open_path(self, path): @@ -200,7 +203,7 @@ def mimeData(self, index_list): return 0 mime_data = MainFrame.NodeMimeData() - new_node = copy_element(self.itemFromIndex(index_list[0]).xml_node) + new_node = copy_node(self.itemFromIndex(index_list[0]).xml_node) mime_data.set_item(new_node.model_item) mime_data.set_node(new_node) mime_data.set_original_item(self.itemFromIndex(index_list[0])) @@ -372,7 +375,7 @@ def __init__(self, child_tag, parent_node, tree_model, settings_dict, select_nod def redo(self): if self.new_child_node is None: - self.new_child_node = elem_factory(self.child_tag, self.parent_node) + self.new_child_node = node_factory(self.child_tag, self.parent_node) defaults_dict = self.settings_dict["Defaults"] if self.child_tag in defaults_dict and defaults_dict[self.child_tag].enabled(): self.new_child_node.properties[defaults_dict[self.child_tag].key()].set_value( @@ -400,7 +403,7 @@ def __init__(self, parent_item, status_bar, tree_model, select_node_signal): self.pasted_node = None def redo(self): - self.pasted_node = copy_element(QApplication.clipboard().mimeData().node()) + self.pasted_node = copy_node(QApplication.clipboard().mimeData().node()) self.parent_item.xml_node.append(self.pasted_node) self.parent_item.appendRow(self.pasted_node.model_item) self.parent_item.sortChildren(0) @@ -432,6 +435,8 @@ def __init__(self): self.menu_Recent_Files.setIcon(QIcon(join(cur_folder, "resources/logos/logo_recent.png"))) self.actionExpand_All.setIcon(QIcon(join(cur_folder, "resources/logos/logo_expand.png"))) self.actionCollapse_All.setIcon(QIcon(join(cur_folder, "resources/logos/logo_collapse.png"))) + self.actionHide_Node.setIcon(QIcon(join(cur_folder, "resources/logos/logo_hide.png"))) + self.actionShow_Node.setIcon(QIcon(join(cur_folder, "resources/logos/logo_show.png"))) # manage undo and redo self.undo_stack = QUndoStack(self) @@ -459,6 +464,8 @@ def __init__(self): self.actionO_ptions.triggered.connect(self.settings) self.action_Refresh.triggered.connect(self.refresh) self.action_Delete.triggered.connect(self.delete) + self.actionHide_Node.triggered.connect(self.hide_node) + self.actionShow_Node.triggered.connect(self.show_node) self.actionHe_lp.triggered.connect(self.help) self.action_About.triggered.connect(lambda _, self_=self: self.about(self_)) self.actionClear.triggered.connect(self.clear_recent_files) @@ -520,7 +527,7 @@ def __init__(self): self.flag_value_completer.setModel(self.flag_value_model) # connect node selected signal - self.current_node = None + self.current_node = None # type: _NodeElement self.select_node.connect( lambda index: self.set_current_node(self.node_tree_model.itemFromIndex(index).xml_node) ) @@ -536,6 +543,22 @@ def __init__(self): lambda: self.button_wizard.setEnabled(False) if self.current_node.wizard is None else self.button_wizard.setEnabled(True) ) + self.select_node.connect( + lambda index: self.actionHide_Node.setEnabled(True) + if self.current_node is not self._config_root and + self.current_node is not self._info_root and + self.current_node not in self.current_node.getparent().hidden_children and + not self.current_node.allowed_instances + else self.actionHide_Node.setEnabled(False) + ) + self.select_node.connect( + lambda index: self.actionShow_Node.setEnabled(True) + if self.current_node is not self._config_root and + self.current_node is not self._info_root and + self.current_node in self.current_node.getparent().hidden_children and + not self.current_node.allowed_instances + else self.actionShow_Node.setEnabled(False) + ) # manage code changed signal self.xml_code_changed.connect(self.update_previews.emit) @@ -565,6 +588,11 @@ def on_custom_context_menu(self, position): self.select_node.emit(index) node_tree_context_menu.addSeparator() node_tree_context_menu.addAction(self.action_Delete) + if self.current_node is not self._config_root and self.current_node is not self._info_root: + if self.current_node in self.current_node.getparent().hidden_children: + node_tree_context_menu.addAction(self.actionShow_Node) + else: + node_tree_context_menu.addAction(self.actionHide_Node) node_tree_context_menu.addSeparator() node_tree_context_menu.addActions([self.actionCopy, self.actionPaste]) node_tree_context_menu.addSeparator() @@ -596,7 +624,7 @@ def copy_item_to_clipboard(self): def paste_item_from_clipboard(self): parent_item = self.node_tree_model.itemFromIndex(self.node_tree_view.selectedIndexes()[0]) - new_node = copy_element(QApplication.clipboard().mimeData().node()) + new_node = copy_node(QApplication.clipboard().mimeData().node()) if not parent_item.xml_node.can_add_child(new_node): self.statusBar().showMessage("This parent is not valid!") else: @@ -639,7 +667,7 @@ def check_updates(self): def update_available_button(): update_button = QPushButton("New Version Available!") update_button.setFlat(True) - update_button.clicked.connect(lambda: web_open("https://github.com/GandaG/fomod-designer/releases/latest")) + update_button.clicked.connect(lambda: open_new_tab("https://github.com/GandaG/fomod-designer/releases/latest")) self.statusBar().addPermanentWidget(update_button) def check_remote(): @@ -673,6 +701,14 @@ def check_remote(): Thread(target=check_remote).start() + def hide_node(self): + if self.current_node is not None: + self.current_node.set_hidden(True) + + def show_node(self): + if self.current_node is not None: + self.current_node.set_hidden(False) + 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) @@ -707,7 +743,7 @@ def open(self, path=""): join(cur_folder, "resources", "mod_schema.xsd"), ) except ValidationError as p: - generic_errorbox(p.title, str(p), p.detailed) + generic_errorbox(p.title, str(p), p.detailed).exec_() if not self.settings_dict["Load"]["validate_ignore"]: return if self.settings_dict["Load"]["warnings"]: @@ -717,7 +753,7 @@ def open(self, path=""): config_root, ) except WarningError as p: - generic_errorbox(p.title, str(p), p.detailed) + generic_errorbox(p.title, str(p), p.detailed).exec_() if not self.settings_dict["Save"]["warn_ignore"]: return else: @@ -744,7 +780,7 @@ def open(self, path=""): self.clear_prop_list() self.button_wizard.setEnabled(False) except (DesignerError, ValidatorError) as p: - generic_errorbox(p.title, str(p), p.detailed) + generic_errorbox(p.title, str(p), p.detailed).exec_() return def save(self): @@ -757,8 +793,8 @@ def save(self): if self._info_root is None and self._config_root is None: return elif not self.undo_stack.isClean(): - sort_elements(self._info_root) - sort_elements(self._config_root) + self._info_root.sort() + self._config_root.sort() if self.settings_dict["Save"]["validate"]: try: validate_tree( @@ -766,7 +802,7 @@ def save(self): join(cur_folder, "resources", "mod_schema.xsd"), ) except ValidationError as e: - generic_errorbox(e.title, str(e), e.detailed) + generic_errorbox(e.title, str(e), e.detailed).exec_() if not self.settings_dict["Save"]["validate_ignore"]: return if self.settings_dict["Save"]["warnings"]: @@ -778,13 +814,13 @@ def save(self): except MissingFolderError: pass except WarningError as e: - generic_errorbox(e.title, str(e), e.detailed) + generic_errorbox(e.title, str(e), e.detailed).exec_() if not self.settings_dict["Save"]["warn_ignore"]: return export(self._info_root, self._config_root, self._package_path) self.undo_stack.setClean() except (DesignerError, ValidatorError) as e: - generic_errorbox(e.title, str(e)) + generic_errorbox(e.title, str(e), e.detailed).exec_() return def settings(self): @@ -806,21 +842,30 @@ def delete(self): """ Deletes the current node in the tree. No effect when using the Basic View. """ - try: - if self.current_node is None: - self.statusBar().showMessage("Can't delete nothing.") - else: - self.undo_stack.push(self.DeleteCommand( - self.current_node, - self.node_tree_model, - self.select_node - )) - except AttributeError: + if self.current_node is None: + self.statusBar().showMessage("Can't delete nothing.") + elif self.current_node.getparent() is None: self.statusBar().showMessage("Can't delete root nodes.") + else: + if self.current_node.is_hidden: + self.current_node.set_hidden(False) + self.undo_stack.push(self.DeleteCommand( + self.current_node, + self.node_tree_model, + self.select_node + )) @staticmethod def help(): - not_implemented() + docs_url = "http://fomod-designer.readthedocs.io/en/stable/index.html" + local_docs = "file://" + abspath(join(cur_folder, "resources", "docs", "index.html")) + try: + if head(docs_url, timeout=0.5).status_code == codes.ok: + open_new_tab(docs_url) + else: + raise ConnectionError() + except (Timeout, ConnectionError): + open_new_tab(local_docs) @staticmethod def about(parent): @@ -893,7 +938,12 @@ def update_children_box(self): if widget is not None: widget.deleteLater() - for child in self.current_node.allowed_children: + children_list = list(self.current_node.allowed_children) + + if self.current_node.tag is not Comment: + children_list.insert(0, NodeComment) + + for child in children_list: new_object = child() child_button = QPushButton(new_object.name) font_button = QFont() @@ -974,11 +1024,18 @@ def update_props_list(self): self.layout_prop_editor.setWidget(prop_index, QFormLayout.LabelRole, label) if type(props[key]) is PropertyText: - def open_plain_editor(line_edit_): + def open_plain_editor(line_edit_, node): dialog_ui = window_plaintexteditor.Ui_Dialog() dialog = QDialog(self) dialog_ui.setupUi(dialog) dialog_ui.edit_text.setPlainText(line_edit_.text()) + if node.tag is Comment: + for sequence in node.forbidden_sequences: + dialog_ui.edit_text.textChanged.connect( + lambda: dialog_ui.edit_text.setText( + dialog_ui.edit_text.toPlainText().replace(sequence, "") + ) if sequence in dialog_ui.edit_text.toPlainText() else None + ) dialog_ui.buttonBox.accepted.connect(dialog.close) dialog_ui.buttonBox.accepted.connect(lambda: line_edit_.setText(dialog_ui.edit_text.toPlainText())) dialog_ui.buttonBox.accepted.connect(line_edit_.editingFinished.emit) @@ -995,6 +1052,13 @@ def open_plain_editor(line_edit_): layout.addWidget(text_button) layout.setContentsMargins(0, 0, 0, 0) text_edit.setText(props[key].value) + if self.current_node.tag is Comment: + for sequence in self.current_node.forbidden_sequences: + text_edit.textChanged.connect( + lambda: text_edit.setText( + text_edit.text().replace(sequence, "") + ) if sequence in text_edit.text() else None + ) text_edit.textChanged.connect(props[key].set_value) text_edit.textChanged[str].connect(self.current_node.write_attribs) text_edit.textChanged[str].connect(self.current_node.update_item_name) @@ -1019,7 +1083,9 @@ def open_plain_editor(line_edit_): text_edit.editingFinished.connect( lambda index=prop_index: og_values.update({index: text_edit.text()}) ) - text_button.clicked.connect(lambda _, line_edit_=text_edit: open_plain_editor(line_edit_)) + text_button.clicked.connect( + lambda _, line_edit_=text_edit, node=self.current_node: open_plain_editor(line_edit_, node) + ) if type(props[key]) is PropertyHTML: def open_plain_editor(line_edit_): @@ -1659,7 +1725,10 @@ def __init__(self, parent): super().__init__(parent=parent) self.setupUi(self) - self.move(parent.window().frameGeometry().topLeft() + parent.window().rect().center() - self.rect().center()) + if parent: + self.move( + parent.window().frameGeometry().topLeft() + parent.window().rect().center() - self.rect().center() + ) self.setWindowFlags(Qt.WindowTitleHint | Qt.Dialog) @@ -1673,11 +1742,440 @@ def __init__(self, parent): self.button.clicked.connect(self.close) -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!") +class PreviewMoGui(QWidget, preview_mo.Ui_Form): + clear_tab_signal = pyqtSignal() + clear_ui_signal = pyqtSignal() + invalid_node_signal = pyqtSignal() + missing_node_signal = pyqtSignal() + set_labels_signal = pyqtSignal([str, str, str, str]) + create_page_signal = pyqtSignal([object]) + + class ScaledLabel(QLabel): + def __init__(self, parent=None): + super().__init__(parent) + self.original_pixmap = None + self.setMinimumSize(320, 200) + + def set_scalable_pixmap(self, pixmap): + self.original_pixmap = pixmap + self.setPixmap(self.original_pixmap.scaled(self.size(), Qt.KeepAspectRatio)) + + def resizeEvent(self, event): + if self.pixmap() and self.original_pixmap: + self.setPixmap(self.original_pixmap.scaled(event.size(), Qt.KeepAspectRatio)) + + class PreviewItem(QStandardItem): + def set_priority(self, value): + self.priority = value + + def __init__(self, mo_preview_layout): + super().__init__() + self.mo_preview_layout = mo_preview_layout + self.setupUi(self) + self.mo_preview_layout.addWidget(self) + self.label_image = self.ScaledLabel(self) + self.splitter_label.addWidget(self.label_image) + self.hide() + + self.button_preview_more.setIcon(QIcon(join(cur_folder, "resources/logos/logo_more.png"))) + self.button_preview_less.setIcon(QIcon(join(cur_folder, "resources/logos/logo_less.png"))) + self.button_preview_more.clicked.connect(self.button_preview_more.hide) + self.button_preview_more.clicked.connect(self.button_preview_less.show) + self.button_preview_more.clicked.connect(self.widget_preview.show) + self.button_preview_less.clicked.connect(self.button_preview_less.hide) + self.button_preview_less.clicked.connect(self.button_preview_more.show) + self.button_preview_less.clicked.connect(self.widget_preview.hide) + self.button_preview_more.clicked.emit() + self.button_results_more.setIcon(QIcon(join(cur_folder, "resources/logos/logo_more.png"))) + self.button_results_less.setIcon(QIcon(join(cur_folder, "resources/logos/logo_less.png"))) + self.button_results_more.clicked.connect(self.button_results_more.hide) + self.button_results_more.clicked.connect(self.button_results_less.show) + self.button_results_more.clicked.connect(self.widget_results.show) + self.button_results_less.clicked.connect(self.button_results_less.hide) + self.button_results_less.clicked.connect(self.button_results_more.show) + self.button_results_less.clicked.connect(self.widget_results.hide) + self.button_results_less.clicked.emit() + + self.model_files = QStandardItemModel() + self.tree_results.expanded.connect( + lambda: self.tree_results.header().resizeSections(QHeaderView.Stretch) + ) + self.tree_results.collapsed.connect( + lambda: self.tree_results.header().resizeSections(QHeaderView.Stretch) + ) + self.tree_results.setContextMenuPolicy(Qt.CustomContextMenu) + self.tree_results.customContextMenuRequested.connect(self.on_custom_context_menu) + self.model_flags = QStandardItemModel() + self.list_flags.expanded.connect( + lambda: self.list_flags.header().resizeSections(QHeaderView.Stretch) + ) + self.list_flags.collapsed.connect( + lambda: self.list_flags.header().resizeSections(QHeaderView.Stretch) + ) + self.reset_models() + + self.label_invalid = QLabel( + "Select an Installation Step node or one of its children to preview its installer page." + ) + self.label_invalid.setAlignment(Qt.AlignCenter) + self.mo_preview_layout.addWidget(self.label_invalid) + self.label_invalid.hide() + + self.label_missing = QLabel( + "In order to preview an installer page, create an Installation Step node." + ) + self.label_missing.setAlignment(Qt.AlignCenter) + self.mo_preview_layout.addWidget(self.label_missing) + self.label_missing.hide() + + self.clear_tab_signal.connect(self.clear_tab) + self.clear_ui_signal.connect(self.clear_ui) + self.invalid_node_signal.connect(self.invalid_node) + self.missing_node_signal.connect(self.missing_node) + self.set_labels_signal.connect(self.set_labels) + self.create_page_signal.connect(self.create_page) + + def on_custom_context_menu(self, position): + node_tree_context_menu = QMenu(self.tree_results) + + action_expand = QAction(QIcon(join(cur_folder, "resources/logos/logo_expand.png")), "Expand All", self) + action_collapse = QAction(QIcon(join(cur_folder, "resources/logos/logo_collapse.png")), "Collapse All", self) + + action_expand.triggered.connect(self.tree_results.expandAll) + action_collapse.triggered.connect(self.tree_results.collapseAll) + + node_tree_context_menu.addActions([action_expand, action_collapse]) + + node_tree_context_menu.move(self.tree_results.mapToGlobal(position)) + node_tree_context_menu.exec_() + + def eventFilter(self, object_, event): + if event.type() == QEvent.HoverEnter: + self.label_description.setText(object_.property("description")) + self.label_image.set_scalable_pixmap(QPixmap(object_.property("image_path"))) + + return QWidget().eventFilter(object_, event) + + def clear_ui(self): + self.label_name.clear() + self.label_author.clear() + self.label_version.clear() + self.label_website.clear() + self.label_description.clear() + self.label_image.clear() + [widget.deleteLater() for widget in [ + self.layout_widget.itemAt(index).widget() for index in range(self.layout_widget.count()) + if self.layout_widget.itemAt(index).widget() + ]] + self.reset_models() + + def reset_models(self): + self.model_files.clear() + self.model_files.setHorizontalHeaderLabels(["Files Preview", "Source", "Plugin"]) + self.model_files_root = QStandardItem(QIcon(join(cur_folder, "resources/logos/logo_folder.png")), "") + self.model_files.appendRow(self.model_files_root) + self.tree_results.setModel(self.model_files) + self.model_flags.clear() + self.model_flags.setHorizontalHeaderLabels(["Flag Label", "Flag Value", "Plugin"]) + self.list_flags.setModel(self.model_flags) + + def clear_tab(self): + for index in reversed(range(self.mo_preview_layout.count())): + widget = self.mo_preview_layout.itemAt(index).widget() + if widget is not None: + widget.hide() + + def invalid_node(self): + self.clear_tab() + self.label_invalid.show() + + def missing_node(self): + self.clear_tab() + self.label_missing.show() + + def set_labels(self, name, author, version, website): + self.label_name.setText(name) + self.label_author.setText(author) + self.label_version.setText(version) + self.label_website.setText("link".format(website)) + + # this is pretty horrendous, need to come up with a better way of doing this. + def create_page(self, page_data): + group_step = QGroupBox(page_data.name) + layout_step = QVBoxLayout() + group_step.setLayout(layout_step) + + check_first_radio = True + for group in page_data.group_list: + group_group = QGroupBox(group.name) + layout_group = QVBoxLayout() + group_group.setLayout(layout_group) + + for plugin in group.plugin_list: + if group.type in ["SelectAny", "SelectAll", "SelectAtLeastOne"]: + button_plugin = QCheckBox(plugin.name, self) + + if group.type == "SelectAll": + button_plugin.setChecked(True) + button_plugin.setEnabled(False) + elif group.type == "SelectAtLeastOne": + button_plugin.toggled.connect( + lambda checked, button=button_plugin: button.setChecked(True) + if not checked and not [ + button for button in [ + layout_group.itemAt(index).widget() for index in range(layout_group.count()) + if layout_group.itemAt(index).widget() + ] if button.isChecked() + ] + else None + ) + + elif group.type in ["SelectExactlyOne", "SelectAtMostOne"]: + button_plugin = QRadioButton(plugin.name, self) + if check_first_radio and not button_plugin.isChecked(): + button_plugin.animateClick(0) + check_first_radio = False + + button_plugin.setProperty("description", plugin.description) + button_plugin.setProperty("image_path", plugin.image_path) + button_plugin.setProperty("file_list", plugin.file_list) + button_plugin.setProperty("folder_list", plugin.folder_list) + button_plugin.setProperty("flag_list", plugin.flag_list) + button_plugin.setProperty("type", plugin.type) + button_plugin.setAttribute(Qt.WA_Hover) + + if plugin.type == "Required": + button_plugin.setEnabled(False) + elif plugin.type == "Recommended": + button_plugin.animateClick(0) if not button_plugin.isChecked() else None + elif plugin.type == "NotUsable": + button_plugin.setChecked(False) + button_plugin.setEnabled(False) + + button_plugin.toggled.connect(self.reset_models) + button_plugin.toggled.connect(self.update_installed_files) + button_plugin.toggled.connect(self.update_set_flags) + + button_plugin.installEventFilter(self) + button_plugin.setObjectName("preview_button") + layout_group.addWidget(button_plugin) + + if group.type == "SelectAtMostOne": + button_none = QRadioButton("None") + layout_group.addWidget(button_none) + + layout_step.addWidget(group_group) + + self.layout_widget.addWidget(group_step) + self.reset_models() + self.update_installed_files() + self.update_set_flags() + self.show() + + def update_installed_files(self): + def recurse_add_items(folder, parent): + for boop in listdir(folder): # I was very tired + if isdir(join(folder, boop)): + folder_item = None + existing_folder_ = self.model_files.findItems(boop, Qt.MatchRecursive) + if existing_folder_: + for boopity in existing_folder_: + if boopity.parent() is parent: + folder_item = boopity + break + if not folder_item: + folder_item = self.PreviewItem( + QIcon(join(cur_folder, "resources/logos/logo_folder.png")), + boop + ) + folder_item.set_priority(folder_.priority) + parent.appendRow([folder_item, QStandardItem(rel_source), QStandardItem(button.text())]) + recurse_add_items(join(folder, boop), folder_item) + + elif isfile(join(folder, boop)): + file_item_ = None + existing_file_ = self.model_files.findItems(boop, Qt.MatchRecursive) + if existing_file_: + for boopity in existing_file_: + if boopity.parent() is parent: + if folder_.priority < boopity.priority: + file_item_ = boopity + break + else: + parent.removeRow(boopity.row()) + break + if not file_item_: + file_item_ = self.PreviewItem( + QIcon(join(cur_folder, "resources/logos/logo_file.png")), + boop + ) + file_item_.set_priority(folder_.priority) + parent.appendRow([file_item_, QStandardItem(rel_source), QStandardItem(button.text())]) + + for button in self.findChildren((QCheckBox, QRadioButton), "preview_button"): + for folder_ in button.property("folder_list"): + if (button.isChecked() and button.property("type") != "NotUsable" or + folder_.always_install or + folder_.install_usable and button.property("type") != "NotUsable" or + button.property("type") == "Required"): + destination = folder_.destination + abs_source = folder_.abs_source + rel_source = folder_.rel_source + parent_item = self.model_files_root + + destination_split = destination.split("/") + if destination_split[0] == ".": + destination_split = destination_split[1:] + for dest_folder in destination_split: + existing_folder_list = self.model_files.findItems(dest_folder, Qt.MatchRecursive) + if existing_folder_list: + for existing_folder in existing_folder_list: + if existing_folder.parent() is parent_item: + parent_item = existing_folder + break + continue + item_ = self.PreviewItem( + QIcon(join(cur_folder, "resources/logos/logo_folder.png")), + dest_folder + ) + item_.set_priority(folder_.priority) + parent_item.appendRow([item_, QStandardItem(), QStandardItem(button.text())]) + parent_item = item_ + + if isdir(abs_source): + recurse_add_items(abs_source, parent_item) + + for file_ in button.property("file_list"): + if (button.isChecked() and button.property("type") != "NotUsable" or + file_.always_install or + file_.install_usable and button.property("type") != "NotUsable" or + button.property("type") == "Required"): + destination = file_.destination + abs_source = file_.abs_source + rel_source = file_.rel_source + parent_item = self.model_files_root + + destination_split = destination.split("/") + if destination_split[0] == ".": + destination_split = destination_split[1:] + for dest_folder in destination_split: + existing_folder_list = self.model_files.findItems(dest_folder, Qt.MatchRecursive) + if existing_folder_list: + for existing_folder in existing_folder_list: + if existing_folder.parent() is parent_item: + parent_item = existing_folder + break + continue + item_ = self.PreviewItem( + QIcon(join(cur_folder, "resources/logos/logo_folder.png")), + dest_folder + ) + item_.set_priority(file_.priority) + parent_item.appendRow([item_, QStandardItem(), QStandardItem(button.text())]) + parent_item = item_ + + source_file = abs_source.split("/")[len(abs_source.split("/")) - 1] + file_item = None + existing_file_list = self.model_files.findItems(source_file, Qt.MatchRecursive) + if existing_file_list: + for existing_file in existing_file_list: + if existing_file.parent() is parent_item: + if file_.priority < existing_file.priority: + file_item = existing_file + break + else: + parent_item.removeRow(existing_file.row()) + break + if not file_item: + file_item = self.PreviewItem( + QIcon(join(cur_folder, "resources/logos/logo_file.png")), + source_file + ) + file_item.set_priority(file_.priority) + parent_item.appendRow([file_item, QStandardItem(rel_source), QStandardItem(button.text())]) + + self.tree_results.header().resizeSections(QHeaderView.Stretch) + + def update_set_flags(self): + for button in self.findChildren((QCheckBox, QRadioButton), "preview_button"): + if button.isChecked(): + for flag in button.property("flag_list"): + flag_label = QStandardItem(flag.label) + flag_value = QStandardItem(flag.value) + flag_plugin = QStandardItem(button.text()) + existing_flag = self.model_flags.findItems(flag.label) + if existing_flag: + previous_flag_row = existing_flag[0].row() + self.model_flags.removeRow(previous_flag_row) + self.model_flags.insertRow(previous_flag_row, [flag_label, flag_value, flag_plugin]) + else: + self.model_flags.appendRow([flag_label, flag_value, flag_plugin]) + + self.list_flags.header().resizeSections(QHeaderView.Stretch) + + +class DefaultsSettings(object): + def __init__(self, key, default_enabled, default_value): + self.__enabled = default_enabled + self.__property_key = key + self.__property_value = default_value + + def __eq__(self, other): + if self.enabled() == other.enabled() and self.value() == other.value() and self.key() == other.key(): + return True + else: + return False + + def set_enabled(self, enabled): + self.__enabled = enabled + + def set_value(self, value): + self.__property_value = value + + def enabled(self): + return self.__enabled + + def value(self): + return self.__property_value + + def key(self): + return self.__property_key + + +default_settings = { + "General": { + "code_refresh": 3, + "show_intro": True, + "show_advanced": False, + "tutorial_advanced": True, + }, + "Appearance": { + "required_colour": "#ba4d0e", + "atleastone_colour": "#d0d02e", + "either_colour": "#ffaa7f", + "style": "", + "palette": "", + }, + "Defaults": { + "installSteps": DefaultsSettings("order", True, "Explicit"), + "optionalFileGroups": DefaultsSettings("order", True, "Explicit"), + "type": DefaultsSettings("name", True, "Optional"), + "defaultType": DefaultsSettings("name", True, "Optional"), + }, + "Load": { + "validate": True, + "validate_ignore": False, + "warnings": True, + "warn_ignore": True, + }, + "Save": { + "validate": True, + "validate_ignore": False, + "warnings": True, + "warn_ignore": True, + }, + "Recent Files": deque(maxlen=5), +} def generic_errorbox(title, text, detail=""): @@ -1693,7 +2191,7 @@ def generic_errorbox(title, text, detail=""): errorbox.setWindowTitle(title) errorbox.setDetailedText(detail) errorbox.setIconPixmap(QPixmap(join(cur_folder, "resources/logos/logo_admin.png"))) - errorbox.exec_() + return errorbox def read_settings(): @@ -1703,82 +2201,20 @@ def read_settings(): :return: The processed settings. """ - class DefaultsSettings(object): - def __init__(self, key, default_enabled, default_value): - self.__enabled = default_enabled - self.__property_key = key - self.__property_value = default_value - - def set_enabled(self, enabled): - self.__enabled = enabled - - def set_value(self, value): - self.__property_value = value - - def enabled(self): - return self.__enabled - - def value(self): - return self.__property_value - - def key(self): - return self.__property_key - def deep_merge(a, b, path=None): """merges b into a""" if path is None: path = [] for key in b: - if key in a: + if key in a: # only accept the keys in default settings if isinstance(a[key], dict) and isinstance(b[key], dict): deep_merge(a[key], b[key], path + [str(key)]) - elif a[key] == b[key]: - pass # same leaf value elif isinstance(b[key], type(a[key])): a[key] = b[key] - elif not isinstance(b[key], type(a[key])): - pass # user has messed with conf files else: - raise Exception('Conflict at {}'.format('.'.join(path + [str(key)]))) - else: - a[key] = b[key] + pass # user has messed with conf files return a - default_settings = { - "General": { - "code_refresh": 3, - "show_intro": True, - "show_advanced": False, - "tutorial_advanced": True, - }, - "Appearance": { - "required_colour": "#ba4d0e", - "atleastone_colour": "#d0d02e", - "either_colour": "#ffaa7f", - "style": "", - "palette": "", - }, - "Defaults": { - "installSteps": DefaultsSettings("order", True, "Explicit"), - "optionalFileGroups": DefaultsSettings("order", True, "Explicit"), - "type": DefaultsSettings("name", True, "Optional"), - "defaultType": DefaultsSettings("name", True, "Optional"), - }, - "Load": { - "validate": True, - "validate_ignore": False, - "warnings": True, - "warn_ignore": True, - }, - "Save": { - "validate": True, - "validate_ignore": False, - "warnings": True, - "warn_ignore": True, - }, - "Recent Files": deque(maxlen=5), - } - try: with open(join(expanduser("~"), ".fomod", ".designer"), "r") as configfile: settings_dict = decode(configfile.read()) diff --git a/designer/io.py b/src/io.py similarity index 83% rename from designer/io.py rename to src/io.py index b9b95c3..0ff4fc6 100644 --- a/designer/io.py +++ b/src/io.py @@ -16,7 +16,7 @@ from os import listdir, makedirs from os.path import join -from lxml.etree import (PythonElementClassLookup, XMLParser, tostring, fromstring, CommentBase, +from lxml.etree import (PythonElementClassLookup, XMLParser, tostring, fromstring, CommentBase, Comment, Element, SubElement, parse, ParseError, ElementTree, CustomElementClassLookup) from .exceptions import MissingFileError, ParserError, TagNotFound @@ -163,7 +163,7 @@ def _validate_child(child): :param child: The child to check. :return: True if valid, False if not. """ - if type(child) in child.getparent().allowed_children: + if type(child) in child.getparent().allowed_children or child.tag is Comment: if child.allowed_instances: instances = 0 for item in child.getparent(): @@ -176,7 +176,7 @@ def _validate_child(child): return False -def elem_factory(tag, parent): +def node_factory(tag, parent=None): """ Function meant as a replacement for the default element factory. @@ -188,35 +188,42 @@ def elem_factory(tag, parent): :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) - - list_ = list_[::-1] - list_[0] = Element(list_[0].tag) - for elem in list_[1:]: - list_[list_.index(elem)] = SubElement(list_[list_.index(elem) - 1], elem.tag) - SubElement(list_[len(list_) - 1], tag) - - root = fromstring(tostring(list_[0]), module_parser) - parsed_list = [] - for elem in root.iterdescendants(): - parsed_list.append(elem) - return parsed_list[len(parsed_list) - 1] - - -def copy_element(element_): - result = elem_factory(element_.tag, element_.getparent()) - element_.write_attribs() - result.text = element_.text - for key in element_.keys(): - result.set(key, element_.get(key)) + if tag is Comment: + from .nodes import NodeComment + return NodeComment() + elif parent is not None: + list_ = [parent] + for elem in parent.iterancestors(): + list_.append(elem) + + list_ = list_[::-1] + list_[0] = Element(list_[0].tag) + for elem in list_[1:]: + list_[list_.index(elem)] = SubElement(list_[list_.index(elem) - 1], elem.tag) + SubElement(list_[len(list_) - 1], tag) + + root = fromstring(tostring(list_[0]), module_parser) + parsed_list = [] + for elem in root.iterdescendants(): + parsed_list.append(elem) + return parsed_list[len(parsed_list) - 1] + else: + return module_parser.makeelement(tag) + + +def copy_node(node, parent=None): + if parent is None: + parent = node.getparent() + result = node_factory(node.tag, parent) + result.text = node.text + for key in node.keys(): + result.set(key, node.get(key)) result.parse_attribs() - for child in element_: - if isinstance(child, CommentBase): - result.append(type(child)(child.text)) + for child in node: + if child.tag is Comment: + result.append(CommentBase(child.text)) else: - new_child = copy_element(child) + new_child = copy_node(child) result.add_child(new_child) result.load_metadata() return result @@ -245,14 +252,15 @@ def import_(package_path): config_root = parse(config_path, parser=module_parser).getroot() for root in (info_root, config_root): - for element in root.iter(tag=Element): + root.sort() + root.model_item.sortChildren(0) + for element in root.iter(): element.parse_attribs() for elem in element: - if not isinstance(elem, CommentBase): - element.model_item.appendRow(elem.model_item) - if not _validate_child(elem): - element.remove_child(elem) + element.model_item.appendRow(elem.model_item) + if not _validate_child(elem): + element.remove_child(elem) element.write_attribs() element.load_metadata() @@ -284,7 +292,16 @@ def export(info_root, config_root, package_path): :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. + :param hidden_nodes: The currently hidden nodes in either tree. """ + hidden_nodes_pairs = [] + for root in (info_root, config_root): + for node in root: + if node.hidden_children: + for hidden_node in node.hidden_children: + hidden_nodes_pairs.append((node, hidden_node, node.index(hidden_node))) + node.remove(hidden_node) + try: fomod_folder = _check_file(package_path, "fomod") except MissingFileError as e: @@ -314,15 +331,5 @@ def export(info_root, config_root, package_path): config_tree = ElementTree(config_root) config_tree.write(configfile, pretty_print=True) - -def sort_elements(root_element): - """ - Sorts the xml elements according to their sort_order member. - - :param root_element: The root element of xml tree. - """ - for parent in root_element.xpath('//*[./*]'): - parent[:] = sorted( - parent, - key=lambda x: x.sort_order + "." + x.user_sort_order if not isinstance(x, CommentBase) else "0" - ) + for pair in hidden_nodes_pairs: + pair[0].insert(pair[2], pair[1]) diff --git a/designer/nodes.py b/src/nodes.py similarity index 74% rename from designer/nodes.py rename to src/nodes.py index 62cdb2f..96f583b 100644 --- a/designer/nodes.py +++ b/src/nodes.py @@ -18,9 +18,10 @@ from collections import OrderedDict from PyQt5.QtGui import QStandardItem from PyQt5.QtCore import Qt -from lxml import etree +from lxml import etree, objectify from jsonpickle import encode, decode, set_encoder_options from json import JSONDecodeError +from .io import copy_node from .wizards import WizardFiles, WizardDepend from .props import PropertyCombo, PropertyInt, PropertyText, PropertyFile, PropertyFolder, PropertyColour, \ PropertyFlagLabel, PropertyFlagValue, PropertyHTML @@ -28,32 +29,70 @@ class NodeComment(etree.CommentBase): - pass + """ + The base class for all comment nodes. + """ + def __init__(self, text=""): + super(NodeComment, self).__init__(text) + + def _init(self): + super()._init() + self.sort_order = "0" + self.user_sort_order = "0".zfill(7) + self.allowed_children = () + self.allowed_instances = 0 + self.wizard = None + self.name = "Comment" + self.is_hidden = False + self.forbidden_sequences = ["", "--"] + self.properties = {"": PropertyText("Comment")} + self.model_item = NodeStandardItem(self) + self.model_item.setForeground(Qt.blue) + self.update_item_name() + + def update_item_name(self): + self.model_item.setText(self.name) if not self.text else self.model_item.setText(self.text[:40]) + + def parse_attribs(self): + self.properties[""].set_value(self.text) + self.update_item_name() + def write_attribs(self): + self.text = self.properties[""].value + if self.text.startswith(""): + self.getparent().model_item.takeRow(self.model_item.row()) + else: + self.model_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsEnabled) + + def load_metadata(self): + pass + + def save_metadata(self): + pass -class _NodeBase(etree.ElementBase): + def sort(self): + pass + + +class _NodeElement(etree.ElementBase): """ The base class for all nodes. Should never be instantiated directly. """ def _init(self): - if type(self) is _NodeBase: + if type(self) is _NodeElement: raise BaseInstanceException(self) super()._init() - def init( - self, - name, - tag, - allowed_instances, - sort_order="0", - allowed_children=None, - properties=None, - wizard=None, - required_children=None, - either_children_group=None, - at_least_one_children_group=None, - name_editable=False, - ): + def init(self, name, tag, allowed_instances, + sort_order="0", + allowed_children=None, + properties=None, + wizard=None, + required_children=None, + either_children_group=None, + at_least_one_children_group=None, + name_editable=False, + ): if not properties: properties = OrderedDict() @@ -74,10 +113,12 @@ def init( self.required_children = required_children self.either_children_group = either_children_group self.at_least_one_children_group = at_least_one_children_group + self.hidden_children = [] + self.is_hidden = False self.allowed_instances = allowed_instances self.wizard = wizard self.metadata = {} - self.user_sort_order = "0" + self.user_sort_order = "0".zfill(7) self.model_item = NodeStandardItem(self) self.model_item.setText(self.name) @@ -101,7 +142,7 @@ def can_add_child(self, child): instances += 1 if instances >= child.allowed_instances: return False - if type(child) in self.allowed_children: + if type(child) in self.allowed_children or child.tag is etree.Comment: return True return False @@ -127,6 +168,23 @@ def remove_child(self, child): self.model_item.takeRow(child.model_item.row()) self.remove(child) + def set_hidden(self, hide: bool): + self.is_hidden = hide + if hide: + self.model_item.setForeground(Qt.green) + self.getparent().hidden_children.append(self) + else: + self.model_item.setForeground(Qt.black) + self.getparent().hidden_children.remove(self) + self.getparent().save_metadata() + + def sort(self): + for parent in self.xpath('//*[./*]'): + parent[:] = sorted( + parent, + key=lambda x: x.sort_order + "." + x.user_sort_order + ) + def parse_attribs(self): """ Reads the values from the BaseElement's attrib dictionary into the node's properties. @@ -150,30 +208,16 @@ def write_attribs(self): self.text = self.properties[key].value continue self.set(key, str(self.properties[key].value)) + if self.is_hidden: + self.getparent().save_metadata() 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. + Override in subclasses as needed. """ - if "name" in self.properties: - if not self.properties["name"].value: - self.model_item.setText(self.name) - return self.name - self.model_item.setText(self.properties["name"].value) - return self.properties["name"].value - elif "source" in self.properties: - if not self.properties["source"].value: - self.model_item.setText(self.name) - return self.name - split_name = self.properties["source"].value.split(sep) - self.model_item.setText(split_name[len(split_name) - 1]) - return split_name[len(split_name) - 1] - else: - self.model_item.setText(self.name) - return self.name + return self.name def load_metadata(self): """ @@ -181,7 +225,7 @@ def load_metadata(self): """ for child in self: if type(child) is NodeComment: - if child.text.split()[0] == "": + if child.text.startswith(""): try: self.metadata = decode(child.text.split(maxsplit=1)[1]) except JSONDecodeError: @@ -189,6 +233,15 @@ def load_metadata(self): self.model_item.setText(self.metadata.get("name", self.update_item_name())) self.user_sort_order = self.metadata.get("user_sort", "0".zfill(7)) + if not self.hidden_children: + hidden_nodes = self.metadata.get("hidden_nodes", []) + for node_string in hidden_nodes: + node_string = node_string.replace("", "-->") + node = copy_node(etree.fromstring(node_string), self) # type: _NodeElement + self.add_child(node) if node.tag is not etree.Comment else self.append(node) + node.set_hidden(True) + self.sort() + self.model_item.sortChildren(0) def save_metadata(self): """ @@ -198,24 +251,40 @@ def save_metadata(self): self.metadata["name"] = self.model_item.text() else: self.metadata.pop("name", None) - if self.user_sort_order: - self.metadata["user_sort"] = self.user_sort_order + + if self.user_sort_order and self.user_sort_order != "0".zfill(7): + self.metadata["user_sort"] = self.user_sort_order.zfill(7) else: self.metadata.pop("user_sort", None) + if self.hidden_children: + self.metadata["hidden_nodes"] = [] + for element in self.hidden_children: + objectify.deannotate(element, cleanup_namespaces=True) + node_string = etree.tostring(element, pretty_print=False, encoding="unicode") + node_string = node_string.replace("", "- ->") + self.metadata["hidden_nodes"].append(node_string) + else: + self.metadata.pop("hidden_nodes", None) + if not self.allowed_children and "" not in self.properties.keys(): return else: meta_comment = None set_encoder_options("json", separators=(',', ':')) for child in self: - if type(child) is NodeComment and self.metadata: - if child.text.split()[0] == "": + if type(child) is NodeComment: + if child.text.startswith(""): meta_comment = child - child.text = " " + encode(self.metadata) + if self.metadata: + child.text = " " + encode(self.metadata) + else: + self.remove(child) - if meta_comment is None: - self.append(NodeComment(" " + encode(self.metadata))) + if meta_comment is None and self.metadata: + meta_comment = NodeComment() + meta_comment.properties[""].set_value(" " + encode(self.metadata)) + self.add_child(meta_comment) class NodeStandardItem(QStandardItem): @@ -233,7 +302,7 @@ def __lt__(self, other): return False -class NodeInfoRoot(_NodeBase): +class NodeInfoRoot(_NodeElement): """ A node for the tag fomod """ @@ -258,7 +327,7 @@ def _init(self): super()._init() -class NodeInfoName(_NodeBase): +class NodeInfoName(_NodeElement): """ A node for the tag Name """ @@ -277,7 +346,7 @@ def _init(self): super()._init() -class NodeInfoAuthor(_NodeBase): +class NodeInfoAuthor(_NodeElement): """ A node for the tag Author """ @@ -296,7 +365,7 @@ def _init(self): super()._init() -class NodeInfoVersion(_NodeBase): +class NodeInfoVersion(_NodeElement): """ A node for the tag Version """ @@ -315,7 +384,7 @@ def _init(self): super()._init() -class NodeInfoID(_NodeBase): +class NodeInfoID(_NodeElement): """ A node for the tag Id """ @@ -334,7 +403,7 @@ def _init(self): super()._init() -class NodeInfoWebsite(_NodeBase): +class NodeInfoWebsite(_NodeElement): """ A node for the tag Website """ @@ -353,7 +422,7 @@ def _init(self): super()._init() -class NodeInfoDescription(_NodeBase): +class NodeInfoDescription(_NodeElement): """ A node for the tag Description """ @@ -372,7 +441,7 @@ def _init(self): super()._init() -class NodeInfoGroup(_NodeBase): +class NodeInfoGroup(_NodeElement): """ A node for the tag Groups """ @@ -391,7 +460,7 @@ def _init(self): super()._init() -class NodeInfoElement(_NodeBase): +class NodeInfoElement(_NodeElement): """ A node for the tag element """ @@ -410,7 +479,7 @@ def _init(self): super()._init() -class NodeConfigRoot(_NodeBase): +class NodeConfigRoot(_NodeElement): """ A node for the tag config """ @@ -454,7 +523,7 @@ def _init(self): super()._init() -class NodeConfigModName(_NodeBase): +class NodeConfigModName(_NodeElement): """ A node for the tag moduleName """ @@ -476,7 +545,7 @@ def _init(self): super()._init() -class NodeConfigModImage(_NodeBase): +class NodeConfigModImage(_NodeElement): """ A node for the tag moduleImage """ @@ -499,7 +568,7 @@ def _init(self): super()._init() -class NodeConfigModDepend(_NodeBase): +class NodeConfigModDepend(_NodeElement): """ A node for the tag moduleDependencies """ @@ -527,7 +596,7 @@ def _init(self): super()._init() -class NodeConfigReqFiles(_NodeBase): +class NodeConfigReqFiles(_NodeElement): """ A node for the tag requiredInstallFiles """ @@ -549,7 +618,7 @@ def _init(self): super()._init() -class NodeConfigInstallSteps(_NodeBase): +class NodeConfigInstallSteps(_NodeElement): """ A node for the tag installSteps """ @@ -577,7 +646,7 @@ def _init(self): super()._init() -class NodeConfigCondInstall(_NodeBase): +class NodeConfigCondInstall(_NodeElement): """ A node for the tag conditionalFileInstalls """ @@ -601,7 +670,7 @@ def _init(self): super()._init() -class NodeConfigDependFile(_NodeBase): +class NodeConfigDependFile(_NodeElement): """ A node for the tag fileDependency """ @@ -621,7 +690,7 @@ def _init(self): super()._init() -class NodeConfigDependFlag(_NodeBase): +class NodeConfigDependFlag(_NodeElement): """ A node for the tag flagDependency """ @@ -641,7 +710,7 @@ def _init(self): super()._init() -class NodeConfigDependGame(_NodeBase): +class NodeConfigDependGame(_NodeElement): """ A node for the tag gameDependency """ @@ -660,7 +729,7 @@ def _init(self): super()._init() -class NodeConfigFile(_NodeBase): +class NodeConfigFile(_NodeElement): """ A node for the tag file """ @@ -682,8 +751,21 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. + + Override in subclasses as needed. + """ + if not self.properties["source"].value: + self.model_item.setText(self.name) + return self.name + split_name = self.properties["source"].value.split(sep) + self.model_item.setText(split_name[len(split_name) - 1]) + return split_name[len(split_name) - 1] -class NodeConfigFolder(_NodeBase): + +class NodeConfigFolder(_NodeElement): """ A node for the tag folder """ @@ -705,8 +787,21 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. -class NodeConfigPatterns(_NodeBase): + Override in subclasses as needed. + """ + if not self.properties["source"].value: + self.model_item.setText(self.name) + return self.name + split_name = self.properties["source"].value.split(sep) + self.model_item.setText(split_name[len(split_name) - 1]) + return split_name[len(split_name) - 1] + + +class NodeConfigPatterns(_NodeElement): """ A node for the tag patterns """ @@ -729,7 +824,7 @@ def _init(self): super()._init() -class NodeConfigPattern(_NodeBase): +class NodeConfigPattern(_NodeElement): """ A node for the tag pattern """ @@ -755,7 +850,7 @@ def _init(self): super()._init() -class NodeConfigFiles(_NodeBase): +class NodeConfigFiles(_NodeElement): """ A node for the tag files """ @@ -777,7 +872,7 @@ def _init(self): super()._init() -class NodeConfigDependencies(_NodeBase): +class NodeConfigDependencies(_NodeElement): """ A node for the tag dependencies """ @@ -805,7 +900,7 @@ def _init(self): super()._init() -class NodeConfigNestedDependencies(_NodeBase): +class NodeConfigNestedDependencies(_NodeElement): """ A node for the tag dependencies (this one refers to the all the dependencies that have a dependencies as a parent). """ @@ -832,7 +927,7 @@ def _init(self): super()._init() -class NodeConfigInstallStep(_NodeBase): +class NodeConfigInstallStep(_NodeElement): """ A node for the tag installStep """ @@ -859,8 +954,20 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. + + Override in subclasses as needed. + """ + if not self.properties["name"].value: + self.model_item.setText(self.name) + return self.name + self.model_item.setText(self.properties["name"].value) + return self.properties["name"].value + -class NodeConfigVisible(_NodeBase): +class NodeConfigVisible(_NodeElement): """ A node for the tag visible """ @@ -888,7 +995,7 @@ def _init(self): super()._init() -class NodeConfigOptGroups(_NodeBase): +class NodeConfigOptGroups(_NodeElement): """ A node for the tag optionalFileGroups """ @@ -916,7 +1023,7 @@ def _init(self): super()._init() -class NodeConfigGroup(_NodeBase): +class NodeConfigGroup(_NodeElement): """ A node for the tag group """ @@ -949,8 +1056,20 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. -class NodeConfigPlugins(_NodeBase): + Override in subclasses as needed. + """ + if not self.properties["name"].value: + self.model_item.setText(self.name) + return self.name + self.model_item.setText(self.properties["name"].value) + return self.properties["name"].value + + +class NodeConfigPlugins(_NodeElement): """ A node for the tag plugins """ @@ -977,7 +1096,7 @@ def _init(self): super()._init() -class NodeConfigPlugin(_NodeBase): +class NodeConfigPlugin(_NodeElement): """ A node for the tag plugin """ @@ -1008,8 +1127,20 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. -class NodeConfigPluginDescription(_NodeBase): + Override in subclasses as needed. + """ + if not self.properties["name"].value: + self.model_item.setText(self.name) + return self.name + self.model_item.setText(self.properties["name"].value) + return self.properties["name"].value + + +class NodeConfigPluginDescription(_NodeElement): """ A node for the tag description """ @@ -1029,7 +1160,7 @@ def _init(self): super()._init() -class NodeConfigImage(_NodeBase): +class NodeConfigImage(_NodeElement): """ A node for the tag image """ @@ -1049,7 +1180,7 @@ def _init(self): super()._init() -class NodeConfigConditionFlags(_NodeBase): +class NodeConfigConditionFlags(_NodeElement): """ A node for the tag conditionFlags """ @@ -1073,7 +1204,7 @@ def _init(self): super()._init() -class NodeConfigTypeDesc(_NodeBase): +class NodeConfigTypeDesc(_NodeElement): """ A node for the tag typeDescriptor """ @@ -1105,7 +1236,7 @@ def can_add_child(self, child): return False -class NodeConfigFlag(_NodeBase): +class NodeConfigFlag(_NodeElement): """ A node for the tag flag """ @@ -1124,8 +1255,20 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. + + Override in subclasses as needed. + """ + if not self.properties["name"].value: + self.model_item.setText(self.name) + return self.name + self.model_item.setText(self.properties["name"].value) + return self.properties["name"].value + -class NodeConfigDependencyType(_NodeBase): +class NodeConfigDependencyType(_NodeElement): """ A node for the tag dependencyType """ @@ -1150,7 +1293,7 @@ def _init(self): super()._init() -class NodeConfigDefaultType(_NodeBase): +class NodeConfigDefaultType(_NodeElement): """ A node for the tag defaultType """ @@ -1169,8 +1312,20 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. -class NodeConfigType(_NodeBase): + Override in subclasses as needed. + """ + if not self.properties["name"].value: + self.model_item.setText(self.name) + return self.name + self.model_item.setText(self.properties["name"].value) + return self.properties["name"].value + + +class NodeConfigType(_NodeElement): """ A node for the tag type """ @@ -1189,8 +1344,20 @@ def _init(self): ) super()._init() + def update_item_name(self): + """ + Updates this node's item's display name. + + Override in subclasses as needed. + """ + if not self.properties["name"].value: + self.model_item.setText(self.name) + return self.name + self.model_item.setText(self.properties["name"].value) + return self.properties["name"].value + -class NodeConfigInstallPatterns(_NodeBase): +class NodeConfigInstallPatterns(_NodeElement): """ A node for the tag patterns """ @@ -1214,7 +1381,7 @@ def _init(self): super()._init() -class NodeConfigInstallPattern(_NodeBase): +class NodeConfigInstallPattern(_NodeElement): """ A node for the tag pattern """ diff --git a/src/previews.py b/src/previews.py new file mode 100644 index 0000000..e671df3 --- /dev/null +++ b/src/previews.py @@ -0,0 +1,286 @@ +#!/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 os.path import join, sep, normpath +from queue import Queue +from PyQt5.QtCore import QThread +from lxml.etree import XML, tostring, Comment +from lxml.objectify import deannotate +from pygments import highlight +from pygments.formatters.html import HtmlFormatter +from pygments.lexers.html import XmlLexer + + +class PreviewDispatcherThread(QThread): + """ + Thread used to dispatch the element to each preview worker thread. + + :param queue: The main queue containing the elements to process. + :param mo_signal: The signal to pass to the MO preview worker, updates the MO preview. + :param nmm_signal: The signal to pass to the NMM preview worker, updates the NMM preview. + :param code_signal: The signal to pass to the code preview worker, updates the code preview. + """ + def __init__(self, queue, code_signal, **kwargs): + super().__init__() + self.queue = queue + self.gui_queue = Queue() + self.code_queue = Queue() + + self.code_thread = PreviewCodeWorker(self.code_queue, code_signal) + self.code_thread.start() + self.gui_thread = PreviewGuiWorker(self.gui_queue, **kwargs) + self.gui_thread.start() + + def run(self): + while True: + # wait for next element + element = self.queue.get() + + if element is not None: + element.write_attribs() + element.load_metadata() + element.sort() + + # dispatch to every queue + self.gui_queue.put(element) + self.code_queue.put(element) + + +class PreviewCodeWorker(QThread): + """ + Takes a xml element, writes the code, highlights it with inline css and returns the html code. + + :param queue: The queue that receives the elements to be processed. + :param return_signal: The signal used to send the return code through. + :return: The highlighted element html code. + """ + def __init__(self, queue, return_signal): + super().__init__() + self.queue = queue + self.return_signal = return_signal + + def run(self): + while True: + # wait for next element + element = self.queue.get() + + if element is None or element.tag is Comment: + self.return_signal.emit("") + continue + + element = XML(tostring(element)) + + # process the element + deannotate(element, cleanup_namespaces=True) + code = tostring(element, encoding="Unicode", pretty_print=True, xml_declaration=False) + self.return_signal.emit(highlight(code, XmlLexer(), HtmlFormatter( + noclasses=True, style="autumn", linenos="table" + ))) + + +class PreviewGuiWorker(QThread): + class InstallStepData(object): + def __init__(self, name): + self.name = name + self.group_list = [] + + def set_group_list(self, group_list): + self.group_list = group_list + + def sort_ascending(self): + self.group_list = sorted(self.group_list, key=lambda x: x.name) + + def sort_descending(self): + self.group_list = sorted(self.group_list, reverse=True, key=lambda x: x.name) + + class GroupData(object): + def __init__(self, name, group_type): + self.name = name + self.type = group_type + self.plugin_list = [] + + def set_plugin_list(self, plugin_list): + self.plugin_list = plugin_list + + def sort_ascending(self): + self.plugin_list = sorted(self.plugin_list, key=lambda x: x.name) + + def sort_descending(self): + self.plugin_list = sorted(self.plugin_list, reverse=True, key=lambda x: x.name) + + class PluginData(object): + def __init__(self, name, description, image_path, file_list, folder_list, flag_list, plugin_type): + self.name = name + self.description = description + self.image_path = image_path + self.file_list = file_list + self.folder_list = folder_list + self.flag_list = flag_list + self.type = plugin_type + + class FileData(object): + def __init__(self, abs_source, rel_source, destination, priority, always_install, install_usable): + self.abs_source = abs_source + self.rel_source = rel_source + self.destination = destination + self.priority = priority + self.always_install = always_install + self.install_usable = install_usable + + class FolderData(FileData): + pass + + class FlagData(object): + def __init__(self, label, value): + self.label = label + self.value = value + + def __init__(self, queue, **kwargs): + super().__init__() + self.queue = queue + self.kwargs = kwargs + + def run(self): + while True: + # wait for next element + element = self.queue.get() + + if element is None: + self.kwargs["gui_worker"].invalid_node_signal.emit() + continue + elif element.tag == "installStep": + pass + elif [elem for elem in element.iterancestors() if elem.tag == "installStep"]: + element = [elem for elem in element.iterancestors() if elem.tag == "installStep"][0] + elif not [elem for elem in self.kwargs["config_root"]().iter() if elem.tag == "installStep"]: + self.kwargs["gui_worker"].missing_node_signal.emit() + continue + else: + self.kwargs["gui_worker"].invalid_node_signal.emit() + continue + + self.kwargs["gui_worker"].clear_tab_signal.emit() + self.kwargs["gui_worker"].clear_ui_signal.emit() + info_name = self.kwargs["info_root"]().find("Name").text \ + if self.kwargs["info_root"]().find("Name") is not None else "" + info_author = self.kwargs["info_root"]().find("Author").text \ + if self.kwargs["info_root"]().find("Author") is not None else "" + info_version = self.kwargs["info_root"]().find("Version").text \ + if self.kwargs["info_root"]().find("Version") is not None else "" + info_website = self.kwargs["info_root"]().find("Website").text \ + if self.kwargs["info_root"]().find("Website") is not None else "" + self.kwargs["gui_worker"].set_labels_signal.emit(info_name, info_author, info_version, info_website) + + step_data = self.InstallStepData(element.get("name")) + opt_group_elem = element.find("optionalFileGroups") + if opt_group_elem is not None: + group_data_list = [] + + for group_elem in opt_group_elem.findall("group"): + group_data = self.GroupData(group_elem.get("name"), group_elem.get("type")) + + plugins_elem = group_elem.find("plugins") + if plugins_elem is not None: + plugin_data_list = [] + + for plugin_elem in plugins_elem.findall("plugin"): + name_ = plugin_elem.get("name") + description_ = plugin_elem.find("description").text \ + if plugin_elem.find("description") is not None else "" + image_ = plugin_elem.find("image").get("path") \ + if plugin_elem.find("image") is not None else "" + if image_: + # normalize path, for some reason normpath wasn't working + image_ = join(self.kwargs["package_path"](), image_).replace("\\", "/") + image_ = image_.replace("/", sep) + + file_data_list = [] + for file_elem in plugin_elem.findall("files/file"): + file_data_list.append( + self.FileData( + normpath(join( + self.kwargs["package_path"](), + file_elem.get("source").replace("\\", "/") + )), + file_elem.get("source"), + normpath(file_elem.get("destination").replace("\\", "/")), + file_elem.get("priority"), + file_elem.get("alwaysInstall"), + file_elem.get("installIfUsable") + ) + ) + + folder_data_list = [] + for folder_elem in plugin_elem.findall("files/folder"): + folder_data_list.append( + self.FolderData( + normpath(join( + self.kwargs["package_path"](), + folder_elem.get("source").replace("\\", "/") + )), + folder_elem.get("source"), + normpath(folder_elem.get("destination").replace("\\", "/")), + folder_elem.get("priority"), + folder_elem.get("alwaysInstall"), + folder_elem.get("installIfUsable") + ) + ) + + flag_data_list = [] + for flag_elem in plugin_elem.findall("conditionFlags/flag"): + flag_data_list.append( + self.FlagData( + flag_elem.get("name"), + flag_elem.text + ) + ) + + type_elem = plugin_elem.find("typeDescriptor/type") + default_type_elem = plugin_elem.find("typeDescriptor/dependencyType/defaultType") + if type_elem is not None: + type_ = type_elem.get("name") + elif default_type_elem is not None: + type_ = default_type_elem.get("name") + else: + type_ = "Required" + + plugin_data_list.append( + self.PluginData( + name_, + description_, + image_, + file_data_list, + folder_data_list, + flag_data_list, + type_ + ) + ) + + group_data.set_plugin_list(plugin_data_list) + if plugins_elem.get("order") == "Ascending": + group_data.sort_ascending() + elif plugins_elem.get("order") == "Descending": + group_data.sort_descending() + + group_data_list.append(group_data) + + step_data.set_group_list(group_data_list) + if opt_group_elem.get("order") == "Ascending": + step_data.sort_ascending() + elif opt_group_elem.get("order") == "Descending": + step_data.sort_descending() + + self.kwargs["gui_worker"].create_page_signal.emit(step_data) diff --git a/designer/props.py b/src/props.py similarity index 100% rename from designer/props.py rename to src/props.py diff --git a/designer/ui_templates/__init__.py b/src/ui_templates/__init__.py similarity index 100% rename from designer/ui_templates/__init__.py rename to src/ui_templates/__init__.py diff --git a/designer/ui_templates/preview_mo.py b/src/ui_templates/preview_mo.py similarity index 100% rename from designer/ui_templates/preview_mo.py rename to src/ui_templates/preview_mo.py diff --git a/designer/ui_templates/tutorial_advanced.py b/src/ui_templates/tutorial_advanced.py similarity index 100% rename from designer/ui_templates/tutorial_advanced.py rename to src/ui_templates/tutorial_advanced.py diff --git a/designer/ui_templates/window_about.py b/src/ui_templates/window_about.py similarity index 100% rename from designer/ui_templates/window_about.py rename to src/ui_templates/window_about.py diff --git a/designer/ui_templates/window_intro.py b/src/ui_templates/window_intro.py similarity index 86% rename from designer/ui_templates/window_intro.py rename to src/ui_templates/window_intro.py index a6c76b1..8391225 100644 --- a/designer/ui_templates/window_intro.py +++ b/src/ui_templates/window_intro.py @@ -63,28 +63,41 @@ def setupUi(self, MainWindow): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.new_button.sizePolicy().hasHeightForWidth()) self.new_button.setSizePolicy(sizePolicy) + self.new_button.setMinimumSize(QtCore.QSize(125, 0)) self.new_button.setAutoFillBackground(False) self.new_button.setObjectName("new_button") self.horizontalLayout.addWidget(self.new_button) spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem4) self.verticalLayout.addLayout(self.horizontalLayout) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout() + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_5.addItem(spacerItem5) + self.button_help = QtWidgets.QPushButton(self.centralwidget) + self.button_help.setMinimumSize(QtCore.QSize(125, 0)) + self.button_help.setObjectName("button_help") + self.horizontalLayout_5.addWidget(self.button_help) + spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_5.addItem(spacerItem6) + self.verticalLayout.addLayout(self.horizontalLayout_5) self.horizontalLayout_4 = QtWidgets.QHBoxLayout() self.horizontalLayout_4.setObjectName("horizontalLayout_4") - spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_4.addItem(spacerItem5) + spacerItem7 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_4.addItem(spacerItem7) self.button_about = QtWidgets.QPushButton(self.centralwidget) + self.button_about.setMinimumSize(QtCore.QSize(125, 0)) self.button_about.setObjectName("button_about") self.horizontalLayout_4.addWidget(self.button_about) - spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_4.addItem(spacerItem6) + spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_4.addItem(spacerItem8) self.verticalLayout.addLayout(self.horizontalLayout_4) - spacerItem7 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem7) + spacerItem9 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem9) self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setObjectName("horizontalLayout_2") - spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem8) + spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem10) self.widget = QtWidgets.QWidget(self.centralwidget) self.widget.setObjectName("widget") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.widget) @@ -99,11 +112,11 @@ def setupUi(self, MainWindow): self.check_advanced.setObjectName("check_advanced") self.verticalLayout_3.addWidget(self.check_advanced) self.horizontalLayout_2.addWidget(self.widget) - spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem9) + spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem11) self.verticalLayout.addLayout(self.horizontalLayout_2) - spacerItem10 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem10) + spacerItem12 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem12) self.recent_label = QtWidgets.QLabel(self.centralwidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -154,6 +167,7 @@ def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate self.version.setText(_translate("MainWindow", "Version 0.0.0")) self.new_button.setText(_translate("MainWindow", "New/Open")) + self.button_help.setText(_translate("MainWindow", "Getting Started")) self.button_about.setText(_translate("MainWindow", "About")) self.check_intro.setText(_translate("MainWindow", "Don\'t show this window again.")) self.check_advanced.setText(_translate("MainWindow", "Show the Advanced View at startup.")) diff --git a/designer/ui_templates/window_mainframe.py b/src/ui_templates/window_mainframe.py similarity index 94% rename from designer/ui_templates/window_mainframe.py rename to src/ui_templates/window_mainframe.py index ca8839d..b2e189d 100644 --- a/designer/ui_templates/window_mainframe.py +++ b/src/ui_templates/window_mainframe.py @@ -231,6 +231,16 @@ def setupUi(self, MainWindow): icon14.addPixmap(QtGui.QPixmap("../logos/logo_collapse.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) self.actionCollapse_All.setIcon(icon14) self.actionCollapse_All.setObjectName("actionCollapse_All") + self.actionHide_Node = QtWidgets.QAction(MainWindow) + icon15 = QtGui.QIcon() + icon15.addPixmap(QtGui.QPixmap("../logos/logo_hide.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.actionHide_Node.setIcon(icon15) + self.actionHide_Node.setObjectName("actionHide_Node") + self.actionShow_Node = QtWidgets.QAction(MainWindow) + icon16 = QtGui.QIcon() + icon16.addPixmap(QtGui.QPixmap("../logos/logo_show.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.actionShow_Node.setIcon(icon16) + self.actionShow_Node.setObjectName("actionShow_Node") self.menu_Recent_Files.addAction(self.actionClear) self.menu_File.addAction(self.action_Open) self.menu_File.addAction(self.action_Save) @@ -238,9 +248,6 @@ def setupUi(self, MainWindow): self.menu_File.addAction(self.menu_Recent_Files.menuAction()) self.menu_File.addSeparator() self.menu_File.addAction(self.actionO_ptions) - self.menu_Tools.addAction(self.actionExpand_All) - self.menu_Tools.addAction(self.actionCollapse_All) - self.menu_Tools.addSeparator() self.menu_Tools.addAction(self.action_Refresh) self.menu_Tools.addSeparator() self.menu_Tools.addAction(self.action_Delete) @@ -327,7 +334,11 @@ def retranslateUi(self, MainWindow): self.actionRedo.setText(_translate("MainWindow", "Redo")) self.actionRedo.setShortcut(_translate("MainWindow", "Ctrl+Shift+Z")) self.actionExpand_All.setText(_translate("MainWindow", "Expand All")) - self.actionExpand_All.setShortcut(_translate("MainWindow", "Ctrl+*")) self.actionCollapse_All.setText(_translate("MainWindow", "Collapse All")) - self.actionCollapse_All.setShortcut(_translate("MainWindow", "Ctrl+.")) + self.actionHide_Node.setText(_translate("MainWindow", "Hide Node")) + self.actionHide_Node.setToolTip(_translate("MainWindow", "Hide Node")) + self.actionHide_Node.setShortcut(_translate("MainWindow", "Ctrl+X")) + self.actionShow_Node.setText(_translate("MainWindow", "Show Node")) + self.actionShow_Node.setToolTip(_translate("MainWindow", "Show Node")) + self.actionShow_Node.setShortcut(_translate("MainWindow", "Ctrl+X")) diff --git a/designer/ui_templates/window_plaintexteditor.py b/src/ui_templates/window_plaintexteditor.py similarity index 100% rename from designer/ui_templates/window_plaintexteditor.py rename to src/ui_templates/window_plaintexteditor.py diff --git a/designer/ui_templates/window_settings.py b/src/ui_templates/window_settings.py similarity index 100% rename from designer/ui_templates/window_settings.py rename to src/ui_templates/window_settings.py diff --git a/designer/ui_templates/window_texteditor.py b/src/ui_templates/window_texteditor.py similarity index 100% rename from designer/ui_templates/window_texteditor.py rename to src/ui_templates/window_texteditor.py diff --git a/designer/ui_templates/wizard_depend_01.py b/src/ui_templates/wizard_depend_01.py similarity index 100% rename from designer/ui_templates/wizard_depend_01.py rename to src/ui_templates/wizard_depend_01.py diff --git a/designer/ui_templates/wizard_depend_depend.py b/src/ui_templates/wizard_depend_depend.py similarity index 100% rename from designer/ui_templates/wizard_depend_depend.py rename to src/ui_templates/wizard_depend_depend.py diff --git a/designer/ui_templates/wizard_depend_depend_depend.py b/src/ui_templates/wizard_depend_depend_depend.py similarity index 100% rename from designer/ui_templates/wizard_depend_depend_depend.py rename to src/ui_templates/wizard_depend_depend_depend.py diff --git a/designer/ui_templates/wizard_depend_depend_file.py b/src/ui_templates/wizard_depend_depend_file.py similarity index 100% rename from designer/ui_templates/wizard_depend_depend_file.py rename to src/ui_templates/wizard_depend_depend_file.py diff --git a/designer/ui_templates/wizard_depend_depend_flag.py b/src/ui_templates/wizard_depend_depend_flag.py similarity index 100% rename from designer/ui_templates/wizard_depend_depend_flag.py rename to src/ui_templates/wizard_depend_depend_flag.py diff --git a/designer/ui_templates/wizard_depend_depend_version.py b/src/ui_templates/wizard_depend_depend_version.py similarity index 100% rename from designer/ui_templates/wizard_depend_depend_version.py rename to src/ui_templates/wizard_depend_depend_version.py diff --git a/designer/ui_templates/wizard_depend_file.py b/src/ui_templates/wizard_depend_file.py similarity index 100% rename from designer/ui_templates/wizard_depend_file.py rename to src/ui_templates/wizard_depend_file.py diff --git a/designer/ui_templates/wizard_depend_flag.py b/src/ui_templates/wizard_depend_flag.py similarity index 100% rename from designer/ui_templates/wizard_depend_flag.py rename to src/ui_templates/wizard_depend_flag.py diff --git a/designer/ui_templates/wizard_files_01.py b/src/ui_templates/wizard_files_01.py similarity index 100% rename from designer/ui_templates/wizard_files_01.py rename to src/ui_templates/wizard_files_01.py diff --git a/designer/ui_templates/wizard_files_item.py b/src/ui_templates/wizard_files_item.py similarity index 100% rename from designer/ui_templates/wizard_files_item.py rename to src/ui_templates/wizard_files_item.py diff --git a/designer/wizards.py b/src/wizards.py similarity index 96% rename from designer/wizards.py rename to src/wizards.py index 4f510ae..8db8058 100644 --- a/designer/wizards.py +++ b/src/wizards.py @@ -21,7 +21,7 @@ from PyQt5.QtGui import QIcon from PyQt5.QtCore import pyqtSignal from . import cur_folder -from .io import elem_factory +from .io import node_factory from .exceptions import BaseInstanceException from .ui_templates import (wizard_files_01, wizard_files_item, wizard_depend_01, wizard_depend_depend, wizard_depend_depend_depend, wizard_depend_depend_file, wizard_depend_depend_flag, @@ -84,7 +84,7 @@ 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) + child = node_factory(element_.tag, element_result) for key in element_.attrib: child.properties[key].set_value(element_.attrib[key]) element_result.add_child(child) @@ -112,10 +112,10 @@ def add_elem(element_, layout): # finish with connections page_ui.button_add_file.clicked.connect( - lambda: add_elem(elem_factory("file", element_result), page_ui.layout_file) + lambda: add_elem(node_factory("file", element_result), page_ui.layout_file) ) page_ui.button_add_folder.clicked.connect( - lambda: add_elem(elem_factory("folder", element_result), page_ui.layout_folder) + lambda: add_elem(node_factory("folder", element_result), page_ui.layout_folder) ) page_ui.finish_button.clicked.connect(lambda: self._process_results(element_result)) page_ui.cancel_button.clicked.connect(self.cancelled.emit) @@ -182,13 +182,13 @@ def copy_depend(element_): if element_.getparent().tag == "dependencies" or \ element_.getparent().tag == "moduleDependencies" or \ element_.getparent().tag == "visible": - result = elem_factory(element_.tag, NodeConfigVisible()) + result = node_factory(element_.tag, NodeConfigVisible()) elif element_.tag == "moduleDependencies": - result = elem_factory(element_.tag, NodeConfigVisible()) + result = node_factory(element_.tag, NodeConfigVisible()) elif element_.tag == "visible": - result = elem_factory(element_.tag, NodeConfigVisible()) + result = node_factory(element_.tag, NodeConfigVisible()) else: - result = elem_factory(element_.tag, NodeConfigRoot()) + result = node_factory(element_.tag, NodeConfigRoot()) element_.write_attribs() for key in element_.keys(): @@ -261,7 +261,7 @@ def add_elem(self, parent_elem, layout, tag="", element_=None): from .nodes import NodeConfigVisible if element_ is None and tag: - child = elem_factory(tag, NodeConfigVisible()) + child = node_factory(tag, NodeConfigVisible()) parent_elem.add_child(child) else: if element_ is None: @@ -294,7 +294,7 @@ def _update_version(self, value, element): elem.write_attribs() else: if value: - elem = elem_factory("gameDependency", element) + elem = node_factory("gameDependency", element) element.add_child(elem) elem.properties["version"].set_value(value) elem.write_attribs() diff --git a/tasks.py b/tasks.py index f07c362..d02c12c 100644 --- a/tasks.py +++ b/tasks.py @@ -39,7 +39,7 @@ def gen_ui(): from os.path import join, isfile from PyQt5.uic import compileUiDir - target_dir = "designer/ui_templates" + target_dir = "src/ui_templates" init_fname = "__init__.py" for item in listdir(target_dir): if item != init_fname and isfile(join(target_dir, item)): @@ -52,7 +52,7 @@ def preview(): run("python dev/pyinstaller-bootstrap.py") -@task +@task() def clean(): from shutil import rmtree @@ -61,7 +61,18 @@ def clean(): print("Build caches cleaned.") -@task(clean, ) +@task() +def docs(): + from os import system, makedirs + from os.path import join + + makedirs(join("resources", "docs"), exist_ok=True) + system("sphinx-build -b html -d {} {} {}".format(join("docs", "build", "doctrees"), + join("docs", "source"), + join("resources", "docs"))) + + +@task(clean, docs) def build(): from platform import system, architecture from shutil import copy @@ -71,7 +82,7 @@ def build(): from fnmatch import fnmatch # set which files will be included within the archive. - included_files = ["LICENSE", "README.md", "CHANGELOG.md", "CONTRIBUTING.md"] + included_files = ["LICENSE"] archive_name = "designer" # the archive's name try: diff --git a/tests/data/incomplete_fomod/fomod/Info.xml b/tests/data/incomplete_fomod/fomod/Info.xml new file mode 100644 index 0000000..4d765f3 --- /dev/null +++ b/tests/data/incomplete_fomod/fomod/Info.xml @@ -0,0 +1,11 @@ + + Test + test_author + boopity + 000000 + + Test + + 0.0.0 + https://test.net + diff --git a/tests/data/invalid_fomod/fomod/Info.xml b/tests/data/invalid_fomod/fomod/Info.xml new file mode 100644 index 0000000..399bca3 --- /dev/null +++ b/tests/data/invalid_fomod/fomod/Info.xml @@ -0,0 +1,11 @@ + + Test + test_author + boopity + 000000 + + Test + + 0.0.0 + https://test.net diff --git a/tests/data/invalid_fomod/fomod/ModuleConfig.xml b/tests/data/invalid_fomod/fomod/ModuleConfig.xml new file mode 100644 index 0000000..dc844a3 --- /dev/null +++ b/tests/data/invalid_fomod/fomod/ModuleConfig.xml @@ -0,0 +1,4 @@ + + Test + Test diff --git a/tests/data/valid_fomod/fomod/Info.xml b/tests/data/valid_fomod/fomod/Info.xml new file mode 100644 index 0000000..4d765f3 --- /dev/null +++ b/tests/data/valid_fomod/fomod/Info.xml @@ -0,0 +1,11 @@ + + Test + test_author + boopity + 000000 + + Test + + 0.0.0 + https://test.net + diff --git a/tests/data/valid_fomod/fomod/ModuleConfig.xml b/tests/data/valid_fomod/fomod/ModuleConfig.xml new file mode 100644 index 0000000..93eb3c4 --- /dev/null +++ b/tests/data/valid_fomod/fomod/ModuleConfig.xml @@ -0,0 +1,76 @@ + + Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 0000000..8640416 --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,205 @@ +#!/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. + +import sys +import os +from datetime import datetime +from copy import deepcopy +from io import StringIO +from unittest.mock import patch, Mock +from jsonpickle import encode, decode +from requests import codes +from json import JSONDecodeError +from PyQt5.QtWidgets import QDialogButtonBox, QMessageBox +from PyQt5.QtCore import Qt +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from src import __version__ +from src.gui import About, read_settings, default_settings, SettingsDialog, generic_errorbox, IntroWindow, \ + MainFrame + + +def test_about_dialog(qtbot): + about_window = About(None) + about_window.show() + qtbot.addWidget(about_window) + + copyright_years = "2016-" + str(datetime.now().year) if datetime.now().year != 2016 else "2016" + copyright_text = "Copyright {} Daniel Nunes".format(copyright_years) + + assert about_window.version.text() == "Version: " + __version__ + assert about_window.copyright.text() == copyright_text + assert about_window.isVisible() + + qtbot.mouseClick(about_window.button, Qt.LeftButton) + assert not about_window.isVisible() + + +@patch('src.gui.open_new_tab') +@patch('src.gui.head') +def test_help(mock_head, mock_new_tab): + mock_response = Mock(spec='status_code') + mock_head.return_value = mock_response + docs_url = "http://fomod-designer.readthedocs.io/en/stable/index.html" + local_docs = "file://" + \ + os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "resources", "docs", "index.html")) + + mock_response.status_code = codes.ok + MainFrame.help() + mock_head.assert_called_once_with(docs_url, timeout=0.5) + mock_new_tab.assert_called_once_with(docs_url) + + mock_response.status_code = codes.forbidden + MainFrame.help() + mock_head.assert_called_with(docs_url, timeout=0.5) + mock_new_tab.assert_called_with(local_docs) + + +def test_errorbox(qtbot): + errorbox = generic_errorbox("Title", "Text", "Detail Text") + errorbox.show() + qtbot.addWidget(errorbox) + + assert errorbox.isVisible() + assert errorbox.windowTitle() == "Title" + assert errorbox.text() == "Text" + assert errorbox.detailedText() == "Detail Text" + + qtbot.mouseClick(errorbox.button(QMessageBox.Ok), Qt.LeftButton) + assert not errorbox.isVisible() + + +@patch('src.gui.open') +def test_read_settings(mock_open): + mock_open.return_value = StringIO(encode(default_settings)) + assert read_settings() == default_settings + + broken_settings = deepcopy(default_settings) + broken_settings["General"] = "random string" # simulate user messing with settings + mock_open.return_value = StringIO(encode(broken_settings)) + assert read_settings() == default_settings + + mock_open.side_effect = FileNotFoundError("mock settings file not existing") + assert read_settings() == default_settings + + mock_open.side_effect = JSONDecodeError( + "mock settings file not being decodable - someone messed with the file", + encode(default_settings), + 10 # just a random value + ) + assert read_settings() == default_settings + + +@patch('src.gui.read_settings') +@patch('src.gui.open') +def test_settings_dialog(mock_open, mock_read_settings, qtbot, tmpdir): + with open(os.path.join(str(tmpdir), "settings_file"), "wt") as settings_file: + settings_file.write(encode(default_settings)) + mock_open.return_value = settings_file + mock_read_settings.return_value = default_settings + settings_window = SettingsDialog(None) + settings_window.show() + qtbot.addWidget(settings_window) + + # check that default settings are properly loaded. + assert settings_window.isVisible() + assert settings_window.settings_dict == default_settings + + assert settings_window.combo_code_refresh.currentIndex() == default_settings["General"]["code_refresh"] + assert settings_window.check_intro.isChecked() == default_settings["General"]["show_intro"] + assert settings_window.check_advanced.isChecked() == default_settings["General"]["show_advanced"] + assert settings_window.check_tutorial.isChecked() == default_settings["General"]["tutorial_advanced"] + + assert settings_window.check_valid_load.isChecked() == default_settings["Load"]["validate"] + assert settings_window.check_valid_load_ignore.isChecked() == default_settings["Load"]["validate_ignore"] + assert settings_window.check_warn_load.isChecked() == default_settings["Load"]["warnings"] + assert settings_window.check_warn_load_ignore.isChecked() == default_settings["Load"]["warn_ignore"] + + assert settings_window.check_valid_save.isChecked() == default_settings["Save"]["validate"] + assert settings_window.check_valid_save_ignore.isChecked() == default_settings["Save"]["validate_ignore"] + assert settings_window.check_warn_save.isChecked() == default_settings["Save"]["warnings"] + assert settings_window.check_warn_save_ignore.isChecked() == default_settings["Save"]["warn_ignore"] + + assert settings_window.check_installSteps.isChecked() == default_settings["Defaults"]["installSteps"].enabled() + assert settings_window.combo_installSteps.isEnabled() == default_settings["Defaults"]["installSteps"].enabled() + assert settings_window.combo_installSteps.currentText() == default_settings["Defaults"]["installSteps"].value() + assert settings_window.check_optionalFileGroups.isChecked() == default_settings["Defaults"][ + "optionalFileGroups"].enabled() + assert settings_window.combo_optionalFileGroups.isEnabled() == default_settings["Defaults"][ + "optionalFileGroups"].enabled() + assert settings_window.combo_optionalFileGroups.currentText() == default_settings["Defaults"][ + "optionalFileGroups"].value() + assert settings_window.check_type.isChecked() == default_settings["Defaults"]["type"].enabled() + assert settings_window.combo_type.isEnabled() == default_settings["Defaults"]["type"].enabled() + assert settings_window.combo_type.currentText() == default_settings["Defaults"]["type"].value() + assert settings_window.check_defaultType.isChecked() == default_settings["Defaults"]["defaultType"].enabled() + assert settings_window.combo_defaultType.isEnabled() == default_settings["Defaults"]["defaultType"].enabled() + assert settings_window.combo_defaultType.currentText() == default_settings["Defaults"]["defaultType"].value() + + assert settings_window.button_colour_required.styleSheet() == "background-color: " + default_settings["Appearance"][ + "required_colour"] + assert settings_window.button_colour_atleastone.styleSheet() == "background-color: " + default_settings[ + "Appearance"]["atleastone_colour"] + assert settings_window.button_colour_either.styleSheet() == "background-color: " + default_settings["Appearance"][ + "either_colour"] + assert (not default_settings["Appearance"]["style"] or + settings_window.combo_style.currentText() == default_settings["Appearance"]["style"]) + assert (not default_settings["Appearance"]["palette"] or + settings_window.combo_palette.currentText() == default_settings["Appearance"]["palette"]) + + # TODO: check if you can simulate clicks on the check boxes, etc. and test new settings. + + os.remove(os.path.join(str(tmpdir), "settings_file")) + with open(os.path.join(str(tmpdir), "settings_file"), "wt") as settings_file: + mock_open.return_value = settings_file + qtbot.mouseClick(settings_window.buttonBox.button(QDialogButtonBox.Ok), Qt.LeftButton) + assert not settings_window.isVisible() + with open(os.path.join(str(tmpdir), "settings_file"), "rt") as settings_file: + assert decode(settings_file.read()) == settings_window.settings_dict + + +@patch("src.gui.read_settings") +def test_intro(mock_read_settings, qtbot): + settings_dict = default_settings + settings_dict["General"]["tutorial_advanced"] = False + mock_read_settings.return_value = settings_dict + intro_window = IntroWindow() + qtbot.addWidget(intro_window) + + assert intro_window.isVisible() + assert intro_window.version.text() == "Version " + __version__ + assert intro_window.windowTitle() == "FOMOD Designer" + + intro_window.open_path("/") + + assert not intro_window.isVisible() + + settings_dict["General"]["show_intro"] = False + mock_read_settings.return_value = settings_dict + intro_window = IntroWindow() + print(intro_window.settings_dict) + qtbot.addWidget(intro_window) + + assert not intro_window.isVisible() + + +def test_mainframe(qtbot): + main_window = MainFrame() + main_window.show() + qtbot.addWidget(main_window) + + assert main_window.isVisible() + + # TODO: actually test this class diff --git a/tests/test_io_nodes_props.py b/tests/test_io_nodes_props.py new file mode 100644 index 0000000..43d5d88 --- /dev/null +++ b/tests/test_io_nodes_props.py @@ -0,0 +1,96 @@ +#!/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. + +import sys, os, lxml, pytest +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from src.io import import_, export, module_parser, new, copy_node, node_factory +from src.exceptions import TagNotFound, ParserError, BaseInstanceException +from src.nodes import _NodeElement +from src.props import _PropertyBase + + +def test_import_export(tmpdir): + tmpdir = str(tmpdir) + + info_root, config_root = import_(os.path.join(os.path.dirname(__file__), "data", "valid_fomod")) + info_root.sort() + config_root.sort() + export(info_root, config_root, tmpdir) + + with open(os.path.join(os.path.dirname(__file__), "data", "valid_fomod", "fomod", "Info.xml")) as info_base: + with open(os.path.join(tmpdir, "fomod", "Info.xml")) as info_exported: + assert info_base.read() == info_exported.read() + with open( + os.path.join( + os.path.dirname(__file__), + "data", + "valid_fomod", + "fomod", + "ModuleConfig.xml" + ) + ) as config_base: + with open(os.path.join(tmpdir, "fomod", "ModuleConfig.xml")) as config_exported: + assert config_base.read() == config_exported.read() + + +def test_exceptions(): + invalid_fomod = "" + with pytest.raises(TagNotFound): + lxml.etree.fromstring(invalid_fomod, parser=module_parser) + + with pytest.raises(ParserError): + import_(os.path.join(os.path.dirname(__file__), "data", "invalid_fomod")) + + with pytest.raises(BaseInstanceException): + _NodeElement() + + with pytest.raises(BaseInstanceException): + _PropertyBase("test", []) + + assert (None, None) == import_(os.path.join(os.path.dirname(__file__), "data", "incomplete_fomod")) + assert (None, None) == import_(os.path.join(os.path.dirname(__file__), "boop")) + + +def test_node_operations(): + base_info = "" + base_config = "" + info_root, config_root = new() + info_root.parse_attribs() + config_root.parse_attribs() + info_root.write_attribs() + config_root.write_attribs() + + assert base_info == lxml.etree.tostring(info_root, encoding="unicode") + assert base_config == lxml.etree.tostring(config_root, encoding="unicode") + + new_elem = node_factory(config_root.allowed_children[0].tag, config_root) + config_root.add_child(new_elem) + new_config_root = copy_node(config_root) + new_config_root.remove_child(new_config_root[0]) + + assert base_config == lxml.etree.tostring(new_config_root, encoding="unicode") + + new_config_root.user_sort_order = "5" + new_sort_order_xml = "" \ + "" \ + "" + + new_config_root.save_metadata() + new_config_root.load_metadata() + + assert new_sort_order_xml == lxml.etree.tostring(new_config_root, encoding="unicode")