From ab2f3f1929c538f82be53635e4f3df47ed14b599 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 13 May 2016 01:57:28 +0000 Subject: [PATCH 01/31] Added help browser gui template. --- resources/templates/browser.ui | 115 +++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 resources/templates/browser.ui diff --git a/resources/templates/browser.ui b/resources/templates/browser.ui new file mode 100644 index 0000000..8a8236b --- /dev/null +++ b/resources/templates/browser.ui @@ -0,0 +1,115 @@ + + + Dialog + + + + 0 + 0 + 640 + 624 + + + + Dialog + + + + + + 6 + + + QLayout::SetMinimumSize + + + 0 + + + + + + 1 + 5 + + + + + + + Backspace + + + + + + + + 1 + 5 + + + + + + + + + + + + 1 + 5 + + + + + + + + + + + + 9 + 5 + + + + + 14 + false + true + + + + Help + + + true + + + Qt::AlignCenter + + + false + + + + + + + + + + 0 + 1 + + + + + + + + + From f9e77418a44410a79f40c16e077b0887d5a2a466 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 9 Jun 2016 18:38:04 +0000 Subject: [PATCH 02/31] Updated python version file. Help is now a static method. --- resources/templates/browser.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/templates/browser.ui b/resources/templates/browser.ui index 8a8236b..46e6ff5 100644 --- a/resources/templates/browser.ui +++ b/resources/templates/browser.ui @@ -52,6 +52,9 @@ + + Home + From 8e33e39a9ae8a5a28c4d47be2d1b9c2f93cdfdf2 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 9 Jun 2016 20:56:30 +0000 Subject: [PATCH 03/31] Added basic documentation structure. --- .gitignore | 2 +- dev/reqs.txt | 10 + docs/Makefile | 225 ++++++++++++++++++++ docs/make.bat | 281 +++++++++++++++++++++++++ docs/source/advanced.rst | 4 + docs/source/api.rst | 11 + docs/source/api/fomod.nodelib.rst | 46 ++++ docs/source/api/fomod.rst | 53 +++++ docs/source/basic.rst | 4 + docs/source/conf.py | 339 ++++++++++++++++++++++++++++++ docs/source/index.rst | 22 ++ setup.cfg | 2 + 12 files changed, 998 insertions(+), 1 deletion(-) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/advanced.rst create mode 100644 docs/source/api.rst create mode 100644 docs/source/api/fomod.nodelib.rst create mode 100644 docs/source/api/fomod.rst create mode 100644 docs/source/basic.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst diff --git a/.gitignore b/.gitignore index f2e6563..d6e117b 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,7 @@ coverage.xml *.log # Sphinx documentation -docs/_build/ +docs/build/ # PyBuilder target/ diff --git a/dev/reqs.txt b/dev/reqs.txt index 3793503..a26e38e 100644 --- a/dev/reqs.txt +++ b/dev/reqs.txt @@ -1,8 +1,18 @@ +alabaster==0.7.8 +Babel==2.3.4 bumpversion==0.5.3 +docutils==0.12 +imagesize==0.7.1 fomod-validator==1.5.2 invoke==0.12.2 +Jinja2==2.8 jsonpickle==0.9.3 lxml==3.5.0 +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 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..4f832f0 --- /dev/null +++ b/docs/source/advanced.rst @@ -0,0 +1,4 @@ +Advanced Usage +============== + +.. Describe advanced usage - advanced view and node tree diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 0000000..d26097e --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,11 @@ +API Documentation +================= + +Welcome to the introductory page to the code documentation. This is a good place to start when you're +thinking of contributing or using this for a project of your own. Below you can find the complete +table of contents with all the project modules: + +.. toctree:: + :glob: + + api/* diff --git a/docs/source/api/fomod.nodelib.rst b/docs/source/api/fomod.nodelib.rst new file mode 100644 index 0000000..8312934 --- /dev/null +++ b/docs/source/api/fomod.nodelib.rst @@ -0,0 +1,46 @@ +fomod.nodelib package +===================== + +Submodules +---------- + +fomod.nodelib.exceptions module +------------------------------- + +.. automodule:: fomod.nodelib.exceptions + :members: + :undoc-members: + :show-inheritance: + +fomod.nodelib.io module +----------------------- + +.. automodule:: fomod.nodelib.io + :members: + :undoc-members: + :show-inheritance: + +fomod.nodelib.nodes module +-------------------------- + +.. automodule:: fomod.nodelib.nodes + :members: + :undoc-members: + :show-inheritance: + +fomod.nodelib.props module +-------------------------- + +.. automodule:: fomod.nodelib.props + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: fomod.nodelib + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/fomod.rst b/docs/source/api/fomod.rst new file mode 100644 index 0000000..864d188 --- /dev/null +++ b/docs/source/api/fomod.rst @@ -0,0 +1,53 @@ +fomod package +============= + +Subpackages +----------- + +.. toctree:: + + fomod.nodelib + +Submodules +---------- + +fomod.exceptions module +----------------------- + +.. automodule:: fomod.exceptions + :members: + :undoc-members: + :show-inheritance: + +fomod.generic module +-------------------- + +.. automodule:: fomod.generic + :members: + :undoc-members: + :show-inheritance: + +fomod.mainwindow module +----------------------- + +.. automodule:: fomod.mainwindow + :members: + :undoc-members: + :show-inheritance: + +fomod.settings module +--------------------- + +.. automodule:: fomod.settings + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: fomod + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/basic.rst b/docs/source/basic.rst new file mode 100644 index 0000000..fb84e2e --- /dev/null +++ b/docs/source/basic.rst @@ -0,0 +1,4 @@ +Basic Usage +=========== + +.. Describe basic usage - basic view and wizards diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..aa58379 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,339 @@ +#!/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 +sys.path.insert(0, os.path.abspath('../fomod')) + +# -- 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. +# +# The short X.Y version. +version = '0.4.1' +# The full version, including alpha/beta/rc tags. +release = '0.4.1' + +# 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 ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# 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/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..8bbdc1e --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,22 @@ +.. FOMOD Designer documentation master file, created by + sphinx-quickstart on Thu Jun 9 19:41:15 2016. + +FOMOD Designer +============== + +.. Brief description here. + +.. Explain basic usage and link it. + +.. Explain advanced usage and link it. + +.. Link to code docs. + +.. toctree:: + :caption: Contents: + :hidden: + :maxdepth: 2 + + Basic Usage + Advanced Usage + API Documentation diff --git a/setup.cfg b/setup.cfg index 5daa5b5..651951b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,8 @@ current_version = 0.7.0 current_build = 0 +[bumpversion:file:docs/source/conf.py] + [bdist_wheel] universal = 1 From 861f301a1f7084c3c6f9021df6f785ae1ac3c4ac Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 01:35:29 +0000 Subject: [PATCH 04/31] Added doc build tasks. Help button now correctly opens help. --- .gitignore | 1 + resources/templates/browser.ui | 8 +++++++- tasks.py | 24 +++++++++++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d6e117b..9857943 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ coverage.xml # Sphinx documentation docs/build/ +resources/docs # PyBuilder target/ diff --git a/resources/templates/browser.ui b/resources/templates/browser.ui index 46e6ff5..fdc21c1 100644 --- a/resources/templates/browser.ui +++ b/resources/templates/browser.ui @@ -11,7 +11,10 @@ - Dialog + Help + + + false @@ -109,6 +112,9 @@ 1 + + true + diff --git a/tasks.py b/tasks.py index f07c362..cd18f6d 100644 --- a/tasks.py +++ b/tasks.py @@ -53,6 +53,17 @@ def preview(): @task +def clean_docs(): + from shutil import rmtree + from os.path import join + + rmtree(join("docs", "build"), ignore_errors=True) + rmtree(join("resources", "docs")) + rmtree(join("docs", "source", "api"), ignore_errors=True) + print("Documentation caches cleaned.") + + +@task() def clean(): from shutil import rmtree @@ -61,7 +72,18 @@ def clean(): print("Build caches cleaned.") -@task(clean, ) +@task(clean_docs) +def docs(): + from os import system + from os.path import join + + system("sphinx-apidoc -To {} {}".format(join("docs", "source", "api"), "fomod")) + 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 From 6f9c14f1ee53e74e737fe1e312b9b472b7325dbe Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 01:52:53 +0000 Subject: [PATCH 05/31] Removed in-app browser. Help button now opens default browser with local help. Switched to RTD sphinx theme. --- dev/reqs.txt | 1 + docs/source/conf.py | 10 ++- resources/templates/browser.ui | 124 --------------------------------- 3 files changed, 10 insertions(+), 125 deletions(-) delete mode 100644 resources/templates/browser.ui diff --git a/dev/reqs.txt b/dev/reqs.txt index a26e38e..0a33436 100644 --- a/dev/reqs.txt +++ b/dev/reqs.txt @@ -16,3 +16,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/docs/source/conf.py b/docs/source/conf.py index aa58379..86e1507 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -123,10 +123,18 @@ # -- 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' +# 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 diff --git a/resources/templates/browser.ui b/resources/templates/browser.ui deleted file mode 100644 index fdc21c1..0000000 --- a/resources/templates/browser.ui +++ /dev/null @@ -1,124 +0,0 @@ - - - Dialog - - - - 0 - 0 - 640 - 624 - - - - Help - - - false - - - - - - 6 - - - QLayout::SetMinimumSize - - - 0 - - - - - - 1 - 5 - - - - - - - Backspace - - - - - - - - 1 - 5 - - - - - - - Home - - - - - - - - 1 - 5 - - - - - - - - - - - - 9 - 5 - - - - - 14 - false - true - - - - Help - - - true - - - Qt::AlignCenter - - - false - - - - - - - - - - 0 - 1 - - - - true - - - - - - - - From 63012098e5edfa273842dac87eb0dfab3d5fb1cf Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 02:09:39 +0000 Subject: [PATCH 06/31] Fixed docs generation. --- tasks.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tasks.py b/tasks.py index cd18f6d..09f58d2 100644 --- a/tasks.py +++ b/tasks.py @@ -58,7 +58,7 @@ def clean_docs(): from os.path import join rmtree(join("docs", "build"), ignore_errors=True) - rmtree(join("resources", "docs")) + rmtree(join("resources", "docs"), ignore_errors=True) rmtree(join("docs", "source", "api"), ignore_errors=True) print("Documentation caches cleaned.") @@ -74,9 +74,10 @@ def clean(): @task(clean_docs) def docs(): - from os import system + from os import system, makedirs from os.path import join + makedirs(join("resources", "docs"), exist_ok=True) system("sphinx-apidoc -To {} {}".format(join("docs", "source", "api"), "fomod")) system("sphinx-build -b html -d {} {} {}".format(join("docs", "build", "doctrees"), join("docs", "source"), From 82eb3fb7548f5d6f6367e969c6b724350c5bafcb Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 02:41:48 +0000 Subject: [PATCH 07/31] Added basic info to index file. --- docs/source/index.rst | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 8bbdc1e..8cf0e7a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,13 +4,40 @@ FOMOD Designer ============== -.. Brief description here. +*A visual editor to quickly create FOMOD installers for Nexus based mods.* -.. Explain basic usage and link it. -.. Explain advanced usage and link it. +Basic Usage ++++++++++++ + +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. + +Continue at :doc:`Basic Usage ` for more information. + + +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 will offer you more customization options than the ``Basic View`` but it's also easier to get lost in big installers. + +Continue at :doc:`Advanced Usage ` for more information. + + +Code Documentation +++++++++++++++++++ + +Want to contribute? Want to learn more about what is happening behind the scenes? Then this is the place for you! +Take a look through the code documentation to familiarize yourself with the code before you dive in the repository. +Contributions and help are always welcome! + +Dive into the :doc:`Code Documentation ` to continue. -.. Link to code docs. .. toctree:: :caption: Contents: From 47747b6ead35f02746de75b78746e1185ffc61e5 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 15:32:09 +0000 Subject: [PATCH 08/31] Added advanced usage draft. --- docs/source/advanced.rst | 149 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 4f832f0..c826ea3 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -2,3 +2,152 @@ Advanced Usage ============== .. Describe advanced usage - advanced view and node tree + +Welcome to the ``Advanced Usage`` section. 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 ++++++++++++++ + +.. Describe the advanced view and how to use it. + + +FOMOD For The Greater 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 for 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 containes text. + +Similarly, any item in the *FOMOD Designer*'s *Object Tree* is a **Node**. Every node has a direct correspondence to a xml tag. These two terms are use interchangeably in the *FOMOD Bible*. + +FOMOD Bible ++++++++++++ + +Info +---- + +Name +---- + +Author +------ + +Description +----------- + +ID +-- + +Categories Group +---------------- + +Category +-------- + +Version +------- + +Website +------- + +Config +------ + +Name +---- + +Image +----- + +Mod Dependencies +---------------- + +File Dependency +--------------- + +Flag Dependency +--------------- + +Game Dependency +--------------- + +Installation Steps +------------------ + +Install Step +------------ + +Visibility +---------- + +Dependencies +------------ + +Option Group +------------ + +Group +----- + +Plugins +------- + +Plugin +------ + +Description +----------- + +Image +----- + +Files +----- + +File +---- + +Folder +------ + +Flags +----- + +Flag +---- + +Type Descriptor +--------------- + +Dependency Type +--------------- + +Patterns +-------- + +Pattern +------- + +Type +---- + +Default Type +------------ + +Mod Requirements +---------------- + +Conditional Installation +------------------------ + +Patterns +-------- + +Pattern +------- From e9b66d7fb992c861da83135d069baba1bdb56273 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 16:50:17 +0000 Subject: [PATCH 09/31] Added basic usage draft. Added RTD badge to readme. Fixed advanced usage headers. --- README.md | 2 +- docs/source/advanced.rst | 98 +++++++++++++++++++++------------------- docs/source/api.rst | 4 +- docs/source/basic.rst | 38 ++++++++++++++++ 4 files changed, 93 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 0c86cb8..45a9921 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # FOMOD Designer -[![Build status](https://ci.appveyor.com/api/projects/status/nep4id3ammekof68?svg=true)](https://ci.appveyor.com/project/GandaG/fomod-editor) [![Build Status](https://travis-ci.org/GandaG/fomod-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) [![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.* diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index c826ea3..36327f9 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -16,138 +16,144 @@ Advanced View FOMOD For The Greater 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 for with the search box on the left sidebar. +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 for 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 containes text. +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 containes text. -Similarly, any item in the *FOMOD Designer*'s *Object Tree* is a **Node**. Every node has a direct correspondence to a xml tag. These two terms are use interchangeably in the *FOMOD Bible*. +Similarly, any item in the *FOMOD Designer*'s *Object Tree* is a **Node**. +Every node has a direct correspondence to a xml tag. +These two terms are use interchangeably in the *FOMOD Bible*. FOMOD Bible -+++++++++++ +----------- Info ----- +.... Name ----- +.... Author ------- +...... Description ------------ +........... ID --- +.. Categories Group ----------------- +................ Category --------- +........ Version -------- +....... Website -------- +....... Config ------- +...... Name ----- +.... Image ------ +..... Mod Dependencies ----------------- +................ File Dependency ---------------- +............... Flag Dependency ---------------- +............... Game Dependency ---------------- +............... Installation Steps ------------------- +.................. Install Step ------------- +............ Visibility ----------- +.......... Dependencies ------------- +............ Option Group ------------- +............ Group ------ +..... Plugins -------- +....... Plugin ------- +...... Description ------------ +........... Image ------ +..... Files ------ +..... File ----- +.... Folder ------- +...... Flags ------ +..... Flag ----- +.... Type Descriptor ---------------- +............... Dependency Type ---------------- +............... Patterns --------- +........ Pattern -------- +....... Type ----- +.... Default Type ------------- +............ Mod Requirements ----------------- +................ Conditional Installation ------------------------- +........................ Patterns --------- +........ Pattern -------- +....... diff --git a/docs/source/api.rst b/docs/source/api.rst index d26097e..bf56e9a 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,5 +1,5 @@ -API Documentation -================= +Code Documentation +================== Welcome to the introductory page to the code documentation. This is a good place to start when you're thinking of contributing or using this for a project of your own. Below you can find the complete diff --git a/docs/source/basic.rst b/docs/source/basic.rst index fb84e2e..929fa2c 100644 --- a/docs/source/basic.rst +++ b/docs/source/basic.rst @@ -2,3 +2,41 @@ Basic Usage =========== .. Describe basic usage - basic view and wizards + +Welcome to the Basic Usage section. This is recommended for first-time users and for everyone who doesn't +want to worry about trees and checking documentation. + +Basic View +++++++++++ + +.. 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. + +Info +---- + +Config +------ + +Installation Steps +------------------ + +Installation Step +----------------- + +Plugin Group +------------ + +Plugin +------ + +Dependencies +------------ + +Files +----- From c6ed9dbce00176c8fdb03f67a772e92f4ca0bd09 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Fri, 10 Jun 2016 23:31:15 +0000 Subject: [PATCH 10/31] Updated readme and index. --- README.md | 20 ++++---------------- docs/source/index.rst | 12 +++++++++++- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 45a9921..8b3058b 100644 --- a/README.md +++ b/README.md @@ -3,26 +3,14 @@ *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. +You can get more information in the official documentation hosted at [Read The Docs](http://fomod-designer.readthedocs.io/en/stable/). ## Contributing +You can check the Code Documentation hosted at [Read The Docs](http://fomod-designer.readthedocs.io/en/stable/api.html). + This repo uses a ***.settings*** file to define all the necessary settings. This file follows this syntax: ``` diff --git a/docs/source/index.rst b/docs/source/index.rst index 8cf0e7a..c50da97 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,6 +7,16 @@ FOMOD Designer *A visual editor to quickly create FOMOD installers for Nexus based mods.* +Installation +++++++++++++ + +1. Download the zip file corresponding to your OS from the `latest release `_; + +2. Extract the files within to a location of your choice; + +3. Run the executable. + + Basic Usage +++++++++++ @@ -46,4 +56,4 @@ Dive into the :doc:`Code Documentation ` to continue. Basic Usage Advanced Usage - API Documentation + Code Documentation From 298191d557338500568624e1017462f9cefce4d7 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sun, 12 Jun 2016 01:22:29 +0000 Subject: [PATCH 11/31] Added structure, tags and description to advanced docs. --- docs/source/advanced.rst | 674 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 665 insertions(+), 9 deletions(-) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 36327f9..5ea44f4 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -1,8 +1,8 @@ +.. include:: + Advanced Usage ============== -.. Describe advanced usage - advanced view and node tree - Welcome to the ``Advanced Usage`` section. 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. @@ -13,147 +13,803 @@ Advanced View .. Describe the advanced view and how to use it. -FOMOD For The Greater Good -++++++++++++++++++++++++++ +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 for 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 containes text. +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 *Object Tree* is a **Node**. Every node has a direct correspondence to a xml tag. These two terms are use interchangeably in the *FOMOD Bible*. -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 +......... + +While this is not strictly enforced with nodes, some tags are enforced a specific order by the FOMOD schema. +When applicable, the possible/required children listed in each node are ordered. + +------------------------------------- Info .... +Tag + fomod + +Description + The root node for the document containing all the information relative to the installer. + +Children + ====================== =========== =========== + Nodes Minimum No. Maximum No. + ====================== =========== =========== + :ref:`info_name_label` 0 |infin| + `Author`_ 0 |infin| + :ref:`info_desc_label` 0 |infin| + `ID`_ 0 |infin| + `Categories Group`_ 0 |infin| + `Version`_ 0 |infin| + `Website`_ 0 |infin| + ====================== =========== =========== + +Properties + *None* + +------------------------------------- + +.. _info_name_label: + Name .... +Tag + Name + +Description + The node that holds the mod's name. + +Children + *None* + +Properties + =============== =============== =============================== + Properties Attributes Description + =============== =============== =============================== + Text [...] The name of the mod. + =============== =============== =============================== + +------------------------------------- + Author ...... +Tag + Author + +Description + The node that holds the mod's author(s). + +Children + *None* + +Properties + =============== =============== =============================== + Properties Attributes 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 + =============== =============== =============================== + Properties Attributes 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 becomes 548961 + +Children + *None* + +Properties + =============== =============== =============================== + Properties Attributes 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 + ====================== =========== =========== + Nodes Minimum No. Maximum No. + ====================== =========== =========== + `Category`_ 0 |infin| + ====================== =========== =========== + +Properties + *None* + +------------------------------------- + Category ........ +Tag + element + +Description + The node that holds one of the mod's category. + +Children + *None* + +Properties + =============== =============== =============================== + Properties Attributes Description + =============== =============== =============================== + Text [...] A category this mod belongs to. + =============== =============== =============================== + +------------------------------------- + Version ....... +Tag + Version + +Description + The node that holds the mod's version. + +Children + *None* + +Properties + =============== =============== =============================== + Properties Attributes Description + =============== =============== =============================== + Text [...] This mod's version. + =============== =============== =============================== + +------------------------------------- + Website ....... +Tag + Website + +Description + The node that holds the mod's home website. + +Children + *None* + +Properties + =============== =============== =============================== + Properties Attributes Description + =============== =============== =============================== + Text [...] The mod's home website. + =============== =============== =============================== + +------------------------------------- + Config ...... +Tag + config + +Description + The main element containing the module configuration info. + +Children + *None* + + +Properties + +------------------------------------- + +.. _config_desc_label: + Name .... +Tag + moduleName + +Description + The name of the module. Used to describe the display properties of the module title. + +Children + *None* + + +Properties + +------------------------------------- + Image ..... +Tag + moduleImage + +Description + The module logo/banner. + + *[Ignored in Mod Organizer]* + +Children + *None* + + +Properties + +------------------------------------- + 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 + *None* + + +Properties + +------------------------------------- + File Dependency ............... +Tag + fileDependency + +Description + Specifies that a mod must be in a specified state. + +Children + *None* + + +Properties + +------------------------------------- + Flag Dependency ............... +Tag + flagDependency + +Description + Specifies that a condition flag must have a specific value. + +Children + *None* + + +Properties + +------------------------------------- + Game Dependency ............... +Tag + gameDependency + +Description + Specifies a minimum required version of the installed game. + + *[Ignored in Mod Organizer]* + +Children + *None* + + +Properties + +------------------------------------- + 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 + *None* + + +Properties + +------------------------------------- + Install Step ............ +Tag + installStep + +Description + A step in the install process containing groups of optional plugins. + +Children + *None* + + +Properties + +------------------------------------- + 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 + *None* + + +Properties + +------------------------------------- + Dependencies ............ +Tag + dependencies + +Description + A dependency that is made up of one or more dependencies. + +Children + *None* + + +Properties + +------------------------------------- + Option Group ............ +Tag + optionalFileGroups + +Description + The list of optional files (or plugins) that may optionally be installed for this module. + +Children + *None* + + +Properties + +------------------------------------- + Group ..... +Tag + group + +Description + A group of plugins for the mod. + +Children + *None* + + +Properties + +------------------------------------- + Plugins ....... +Tag + plugins + +Description + The list of plugins in the group. + +Children + *None* + + +Properties + +------------------------------------- + Plugin ...... +Tag + plugin + +Description + A mod plugin belonging to a group. + +Children + *None* + + +Properties + +------------------------------------- + Description ........... +Tag + description + +Description + A description of the plugin. + +Children + *None* + + +Properties + +------------------------------------- + Image ..... +Tag + image + +Description + The optional image associated with a plugin. + +Children + *None* + + +Properties + +------------------------------------- + Files ..... +Tag + files + +Description + A list of files and folders to be installed. + +Children + *None* + + +Properties + +------------------------------------- + File .... +Tag + file + +Description + A file belonging to the plugin or module. + +Children + *None* + + +Properties + +------------------------------------- + Folder ...... +Tag + folder + +Description + A folder belonging to the plugin or module. + +Children + *None* + + +Properties + +------------------------------------- + Flags ..... +Tag + conditionFlags + +Description + The list of condition flags to set if the plugin is in the appropriate state. + +Children + *None* + + +Properties + +------------------------------------- + Flag .... +Tag + flag + +Description + A condition flag to set if the plugin is selected. + +Children + *None* + + +Properties + +------------------------------------- + Type Descriptor ............... +Tag + typeDescriptor + +Description + Describes the type of a plugin. + +Children + *None* + + +Properties + +------------------------------------- + Dependency Type ............... +Tag + dependencyType + +Description + Used when the plugin type is dependent upon the state of other mods. + +Children + *None* + + +Properties + +------------------------------------- + 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 + *None* + + +Properties + +------------------------------------- + Pattern ....... +Tag + pattern + +Description + A specific pattern of mod files and condition flags against which to match the user's installation. + +Children + *None* + + +Properties + +------------------------------------- + Type .... +Tag + type + +Description + The type of the plugin. + +Children + *None* + + +Properties + +------------------------------------- + Default Type ............ +Tag + defaultType + +Description + The default type of the plugin used if none of the specified dependency states are satisfied. + +Children + *None* + + +Properties + +------------------------------------- + Mod Requirements ................ +Tag + requiredInstallFiles + +Description + The list of files and folders that must be installed for this module. + +Children + *None* + + +Properties + +------------------------------------- + Conditional Installation ........................ +Tag + conditionalFileInstalls + +Description + The list of optional files that may optionally be installed for this module, based on condition flags. + +Children + *None* + + +Properties + +------------------------------------- + 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 + *None* + + +Properties + +------------------------------------- + Pattern ....... + +Tag + pattern + +Description + A specific pattern of mod files and condition flags against which to match the user's installation. + +Children + *None* + + +Properties + From ecf14098733cd3e8abadabb8a2fd52f436ef226b Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 14 Jul 2016 19:54:49 +0000 Subject: [PATCH 12/31] Started test suite. Fixed issue with custom element factory. --- .travis.yml | 10 ++ README.md | 3 +- appveyor.yml | 8 ++ designer/io.py | 35 +++---- designer/nodes.py | 2 +- dev/reqs.txt | 4 + dev/travis-test.sh | 24 +++++ setup.cfg | 4 +- tasks.py | 17 +++- tests/data/incomplete_fomod/fomod/Info.xml | 11 +++ tests/data/invalid_fomod/fomod/Info.xml | 11 +++ .../data/invalid_fomod/fomod/ModuleConfig.xml | 4 + tests/data/valid_fomod/fomod/Info.xml | 11 +++ tests/data/valid_fomod/fomod/ModuleConfig.xml | 76 +++++++++++++++ tests/test_io_nodes_props.py | 96 +++++++++++++++++++ 15 files changed, 296 insertions(+), 20 deletions(-) create mode 100644 dev/travis-test.sh create mode 100644 tests/data/incomplete_fomod/fomod/Info.xml create mode 100644 tests/data/invalid_fomod/fomod/Info.xml create mode 100644 tests/data/invalid_fomod/fomod/ModuleConfig.xml create mode 100644 tests/data/valid_fomod/fomod/Info.xml create mode 100644 tests/data/valid_fomod/fomod/ModuleConfig.xml create mode 100644 tests/test_io_nodes_props.py diff --git a/.travis.yml b/.travis.yml index cc350d6..85f4c05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,22 @@ language: python sudo: required dist: trusty +branches: + only: + - master + - develop install: - chmod +x ./dev/travis-bootstrap.sh - ./dev/travis-bootstrap.sh + - pip install coveralls script: + - chmod +x ./dev/travis-test.sh + - ./dev/travis-test.sh + +after_success: + - coveralls - chmod +x ./dev/travis-build.sh - ./dev/travis-build.sh diff --git a/README.md b/README.md index 0c86cb8..91c01ed 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # 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) + *A visual editor to quickly create FOMOD installers for Nexus based mods.* diff --git a/appveyor.yml b/appveyor.yml index fd27717..7f5befb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,17 @@ +branches: + only: + - master + - develop + install: - .\dev\appveyor-bootstrap.bat build: off test_script: + - inv test + +after_test: - inv build artifacts: diff --git a/designer/io.py b/designer/io.py index b9b95c3..f2a4d3e 100644 --- a/designer/io.py +++ b/designer/io.py @@ -176,7 +176,7 @@ def _validate_child(child): return False -def elem_factory(tag, parent): +def elem_factory(tag, parent=None): """ Function meant as a replacement for the default element factory. @@ -188,21 +188,24 @@ 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] + if 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_element(element_): diff --git a/designer/nodes.py b/designer/nodes.py index 62cdb2f..4ebf2ca 100644 --- a/designer/nodes.py +++ b/designer/nodes.py @@ -199,7 +199,7 @@ def save_metadata(self): else: self.metadata.pop("name", None) if self.user_sort_order: - self.metadata["user_sort"] = self.user_sort_order + self.metadata["user_sort"] = self.user_sort_order.zfill(7) else: self.metadata.pop("user_sort", None) diff --git a/dev/reqs.txt b/dev/reqs.txt index bbfc2f1..7c97402 100644 --- a/dev/reqs.txt +++ b/dev/reqs.txt @@ -1,8 +1,12 @@ bumpversion==0.5.3 +coverage==4.1 fomod-validator==1.5.3 invoke==0.12.2 jsonpickle==0.9.3 lxml==3.5.0 +py==1.4.31 Pygments==2.1.3 PyInstaller==3.1.1 +pytest==2.9.2 +pytest-cov==2.3.0 requests==2.10.0 diff --git a/dev/travis-test.sh b/dev/travis-test.sh new file mode 100644 index 0000000..b510cf6 --- /dev/null +++ b/dev/travis-test.sh @@ -0,0 +1,24 @@ +#!/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 + +invoke test diff --git a/setup.cfg b/setup.cfg index bb95662..719473f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,5 +3,7 @@ current_version = 0.7.2 current_build = 0 [bdist_wheel] -universal = 1 +universal = 0 +[coverage:run] +omit = designer/ui_templates/* diff --git a/tasks.py b/tasks.py index f07c362..f88b3a3 100644 --- a/tasks.py +++ b/tasks.py @@ -61,7 +61,22 @@ def clean(): print("Build caches cleaned.") -@task(clean, ) +@task +def clean_test(): + from shutil import rmtree + + rmtree("htmlcov", ignore_errors=True) + print("Test caches cleaned.") + + +@task(clean_test) +def test(): + from os import system + + system("py.test --cov=designer --cov-report html --cov-report term -vv tests/") + + +@task(clean) def build(): from platform import system, architecture from shutil import copy 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_io_nodes_props.py b/tests/test_io_nodes_props.py new file mode 100644 index 0000000..6505482 --- /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 designer.io import import_, export, module_parser, sort_elements, new, copy_element, elem_factory +from designer.exceptions import TagNotFound, ParserError, BaseInstanceException +from designer.nodes import _NodeBase +from designer.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")) + sort_elements(info_root) + sort_elements(config_root) + 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): + _NodeBase() + + 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 = elem_factory(config_root.allowed_children[0].tag, config_root) + config_root.add_child(new_elem) + new_config_root = copy_element(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") From af275932a65e513f0e4e0edc9d8baedca2c138bb Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 14 Jul 2016 21:51:07 +0000 Subject: [PATCH 13/31] Moved preview gui worker to gui module. --- designer/gui.py | 387 ++++++++++++++++++++++++++++++++++++++++++- designer/previews.py | 382 +----------------------------------------- 2 files changed, 382 insertions(+), 387 deletions(-) diff --git a/designer/gui.py b/designer/gui.py index a7105fd..49b4b19 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -14,8 +14,8 @@ # 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 from io import BytesIO from threading import Thread from queue import Queue @@ -27,20 +27,21 @@ from lxml.etree import parse, tostring 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 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 .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): @@ -1673,6 +1674,378 @@ def __init__(self, parent): self.button.clicked.connect(self.close) +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) + + def not_implemented(): """ A convenience function for something that has not yet been implemented. diff --git a/designer/previews.py b/designer/previews.py index 9cca8bf..6b3fdc3 100644 --- a/designer/previews.py +++ b/designer/previews.py @@ -14,21 +14,15 @@ # 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 os.path import join, sep, normpath 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 PyQt5.QtCore import QThread 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): @@ -291,375 +285,3 @@ def run(self): 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) From ec54801691b351833ba49cd2d367ed0e3d2ec643 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sat, 30 Jul 2016 15:03:16 +0000 Subject: [PATCH 14/31] Closes #22. Added initial test framework. --- designer/gui.py | 151 +++++++++++++++++++------------------ setup.cfg | 1 + tests/test_gui.py | 184 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 263 insertions(+), 73 deletions(-) create mode 100644 tests/test_gui.py diff --git a/designer/gui.py b/designer/gui.py index 49b4b19..d006e2e 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -708,7 +708,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"]: @@ -718,7 +718,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: @@ -745,7 +745,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): @@ -767,7 +767,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"]: @@ -779,13 +779,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): @@ -1660,7 +1660,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) @@ -2046,11 +2049,75 @@ def update_set_flags(self): 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 not_implemented(): """ A convenience function for something that has not yet been implemented. """ - generic_errorbox("Nope", "Sorry, this part hasn't been implemented yet!") + return generic_errorbox("Nope", "Sorry, this part hasn't been implemented yet!") def generic_errorbox(title, text, detail=""): @@ -2066,7 +2133,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(): @@ -2076,82 +2143,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/setup.cfg b/setup.cfg index 719473f..428fa45 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,3 +7,4 @@ universal = 0 [coverage:run] omit = designer/ui_templates/* + diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 0000000..9615ee7 --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,184 @@ +#!/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 +from jsonpickle import encode, decode +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 designer import __version__ +from designer.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() + + +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('designer.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('designer.gui.read_settings') +@patch('designer.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("designer.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 From ffeae157c126c6687f30be5ac4579a366f4e5649 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sat, 30 Jul 2016 18:39:48 +0000 Subject: [PATCH 15/31] Improved node name updating. --- designer/nodes.py | 118 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 18 deletions(-) diff --git a/designer/nodes.py b/designer/nodes.py index 4ebf2ca..bd159e3 100644 --- a/designer/nodes.py +++ b/designer/nodes.py @@ -155,25 +155,9 @@ 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): """ @@ -682,6 +666,19 @@ 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): """ @@ -705,6 +702,19 @@ 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 NodeConfigPatterns(_NodeBase): """ @@ -859,6 +869,18 @@ 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): """ @@ -949,6 +971,18 @@ 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 NodeConfigPlugins(_NodeBase): """ @@ -1008,6 +1042,18 @@ 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 NodeConfigPluginDescription(_NodeBase): """ @@ -1124,6 +1170,18 @@ 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): """ @@ -1169,6 +1227,18 @@ 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 NodeConfigType(_NodeBase): """ @@ -1189,6 +1259,18 @@ 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): """ From ae6f283c439ba1c7c2e27332cda10318b3d61c9a Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Sat, 30 Jul 2016 23:23:21 +0000 Subject: [PATCH 16/31] Added p4merge as the default mergetool. --- dev/vagrant-bootstrap.sh | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) 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 From 817e583bcae2b0a422433d5b7afee60b2fe2b975 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 1 Aug 2016 20:51:23 +0000 Subject: [PATCH 17/31] Reorganized docs index. Added FAQ, installation instructions and getting started. Added ignore rule for the auto generated api docs. Added exclusion for the compiled ui. Fixed path insertion in sphinx conf file. --- .gitignore | 1 + docs/source/advanced.rst | 7 ++- docs/source/api.rst | 6 +- docs/source/api/fomod.nodelib.rst | 46 --------------- docs/source/api/fomod.rst | 53 ----------------- docs/source/basic.rst | 6 +- docs/source/conf.py | 2 +- docs/source/faq.rst | 29 ++++++++++ docs/source/getting_started.rst | 21 +++++++ docs/source/index.rst | 52 ++--------------- docs/source/install.rst | 96 +++++++++++++++++++++++++++++++ tasks.py | 3 +- 12 files changed, 170 insertions(+), 152 deletions(-) delete mode 100644 docs/source/api/fomod.nodelib.rst delete mode 100644 docs/source/api/fomod.rst create mode 100644 docs/source/faq.rst create mode 100644 docs/source/getting_started.rst create mode 100644 docs/source/install.rst diff --git a/.gitignore b/.gitignore index 9857943..acb4113 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ coverage.xml # Sphinx documentation docs/build/ +docs/source/api resources/docs # PyBuilder diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 5ea44f4..ab8d334 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -3,7 +3,12 @@ Advanced Usage ============== -Welcome to the ``Advanced Usage`` section. This is recommended for people who already know how to create/modify XML +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. diff --git a/docs/source/api.rst b/docs/source/api.rst index bf56e9a..181993b 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,7 +1,11 @@ Code Documentation ================== -Welcome to the introductory page to the code documentation. This is a good place to start when you're +Want to contribute? Want to learn more about what is happening behind the scenes? Then this is the place for you! +Take a look through the code documentation to familiarize yourself with the code before you dive in the repository. +Contributions and help are always welcome! + +This is a good place to start when you're thinking of contributing or using this for a project of your own. Below you can find the complete table of contents with all the project modules: diff --git a/docs/source/api/fomod.nodelib.rst b/docs/source/api/fomod.nodelib.rst deleted file mode 100644 index 8312934..0000000 --- a/docs/source/api/fomod.nodelib.rst +++ /dev/null @@ -1,46 +0,0 @@ -fomod.nodelib package -===================== - -Submodules ----------- - -fomod.nodelib.exceptions module -------------------------------- - -.. automodule:: fomod.nodelib.exceptions - :members: - :undoc-members: - :show-inheritance: - -fomod.nodelib.io module ------------------------ - -.. automodule:: fomod.nodelib.io - :members: - :undoc-members: - :show-inheritance: - -fomod.nodelib.nodes module --------------------------- - -.. automodule:: fomod.nodelib.nodes - :members: - :undoc-members: - :show-inheritance: - -fomod.nodelib.props module --------------------------- - -.. automodule:: fomod.nodelib.props - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: fomod.nodelib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/fomod.rst b/docs/source/api/fomod.rst deleted file mode 100644 index 864d188..0000000 --- a/docs/source/api/fomod.rst +++ /dev/null @@ -1,53 +0,0 @@ -fomod package -============= - -Subpackages ------------ - -.. toctree:: - - fomod.nodelib - -Submodules ----------- - -fomod.exceptions module ------------------------ - -.. automodule:: fomod.exceptions - :members: - :undoc-members: - :show-inheritance: - -fomod.generic module --------------------- - -.. automodule:: fomod.generic - :members: - :undoc-members: - :show-inheritance: - -fomod.mainwindow module ------------------------ - -.. automodule:: fomod.mainwindow - :members: - :undoc-members: - :show-inheritance: - -fomod.settings module ---------------------- - -.. automodule:: fomod.settings - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: fomod - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/basic.rst b/docs/source/basic.rst index 929fa2c..6feda2b 100644 --- a/docs/source/basic.rst +++ b/docs/source/basic.rst @@ -3,8 +3,10 @@ Basic Usage .. Describe basic usage - basic view and wizards -Welcome to the Basic Usage section. This is recommended for first-time users and for everyone who doesn't -want to worry about trees and checking documentation. +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 ++++++++++ diff --git a/docs/source/conf.py b/docs/source/conf.py index 86e1507..3a4e55b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ # import os import sys -sys.path.insert(0, os.path.abspath('../fomod')) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) # -- General configuration ------------------------------------------------ diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 0000000..73c3848 --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,29 @@ +Frequently Asked Questions +========================== + +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/getting_started.rst b/docs/source/getting_started.rst new file mode 100644 index 0000000..6c8bc83 --- /dev/null +++ b/docs/source/getting_started.rst @@ -0,0 +1,21 @@ +Getting Started +=============== + +Welcome to the *FOMOD Designer*! 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 :doc:`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 `. diff --git a/docs/source/index.rst b/docs/source/index.rst index c50da97..c9cf371 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,59 +1,17 @@ .. FOMOD Designer documentation master file, created by sphinx-quickstart on Thu Jun 9 19:41:15 2016. -FOMOD Designer -============== - -*A visual editor to quickly create FOMOD installers for Nexus based mods.* - - -Installation -++++++++++++ - -1. Download the zip file corresponding to your OS from the `latest release `_; - -2. Extract the files within to a location of your choice; - -3. Run the executable. - - -Basic Usage -+++++++++++ - -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. - -Continue at :doc:`Basic Usage ` for more information. - - -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 will offer you more customization options than the ``Basic View`` but it's also easier to get lost in big installers. - -Continue at :doc:`Advanced Usage ` for more information. - - -Code Documentation -++++++++++++++++++ - -Want to contribute? Want to learn more about what is happening behind the scenes? Then this is the place for you! -Take a look through the code documentation to familiarize yourself with the code before you dive in the repository. -Contributions and help are always welcome! - -Dive into the :doc:`Code Documentation ` to continue. +.. include:: getting_started.rst .. toctree:: :caption: Contents: :hidden: - :maxdepth: 2 + :maxdepth: 1 + Getting Started + Installation Basic Usage Advanced Usage Code Documentation + F.A.Q. diff --git a/docs/source/install.rst b/docs/source/install.rst new file mode 100644 index 0000000..3fb4c1b --- /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/tasks.py b/tasks.py index 09f58d2..b4df7b4 100644 --- a/tasks.py +++ b/tasks.py @@ -78,7 +78,8 @@ def docs(): from os.path import join makedirs(join("resources", "docs"), exist_ok=True) - system("sphinx-apidoc -To {} {}".format(join("docs", "source", "api"), "fomod")) + system("sphinx-apidoc -TPMeo {} {} {}".format(join("docs", "source", "api"), "designer", + join("designer", "ui_templates"))) system("sphinx-build -b html -d {} {} {}".format(join("docs", "build", "doctrees"), join("docs", "source"), join("resources", "docs"))) From cce2f0af3b14b5795f2efac97af70755e9204264 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 2 Aug 2016 21:03:11 +0000 Subject: [PATCH 18/31] Completed Advanced Usage document. --- docs/source/advanced.rst | 621 ++++++++++++++++++++++++++++++--------- 1 file changed, 489 insertions(+), 132 deletions(-) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index ab8d334..84b798f 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -1,46 +1,61 @@ -.. include:: - Advanced Usage ============== -For the advanced users and everyone who knows their way around a FOMOD installer. +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. +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. +*Basic View* offers. Advanced View +++++++++++++ -.. Describe the advanced view and how to use it. +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 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 for with the search box on the left sidebar. +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 +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 *Object Tree* is a **Node**. +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: +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* @@ -57,13 +72,20 @@ the properties are always followed by the corresponding attribute in square brac Tag Order ......... -While this is not strictly enforced with nodes, some tags are enforced a specific order by the FOMOD schema. +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 @@ -72,17 +94,17 @@ Description The root node for the document containing all the information relative to the installer. Children - ====================== =========== =========== - Nodes Minimum No. Maximum No. - ====================== =========== =========== - :ref:`info_name_label` 0 |infin| - `Author`_ 0 |infin| - :ref:`info_desc_label` 0 |infin| - `ID`_ 0 |infin| - `Categories Group`_ 0 |infin| - `Version`_ 0 |infin| - `Website`_ 0 |infin| - ====================== =========== =========== + ====================== ========== + 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* @@ -92,7 +114,7 @@ Properties .. _info_name_label: Name -.... +---- Tag Name @@ -105,7 +127,7 @@ Children Properties =============== =============== =============================== - Properties Attributes Description + Property Attribute Description =============== =============== =============================== Text [...] The name of the mod. =============== =============== =============================== @@ -113,7 +135,7 @@ Properties ------------------------------------- Author -...... +------ Tag Author @@ -126,7 +148,7 @@ Children Properties =============== =============== =============================== - Properties Attributes Description + Property Attribute Description =============== =============== =============================== Text [...] The author(s) of the mod. =============== =============== =============================== @@ -136,7 +158,7 @@ Properties .. _info_desc_label: Description -........... +----------- Tag Description @@ -149,7 +171,7 @@ Children Properties =============== =============== =============================== - Properties Attributes Description + Property Attribute Description =============== =============== =============================== Text [...] The description of the mod. =============== =============== =============================== @@ -157,7 +179,7 @@ Properties ------------------------------------- ID -.. +-- Tag Id @@ -166,14 +188,14 @@ 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 becomes 548961 + Nexus mod link: http://www.nexusmods.com/skyrim/mods/548961 -> ID's text is 548961 Children *None* Properties =============== =============== =============================== - Properties Attributes Description + Property Attribute Description =============== =============== =============================== Text [...] The ID of the mod. =============== =============== =============================== @@ -181,7 +203,7 @@ Properties ------------------------------------- Categories Group -................ +---------------- Tag Groups @@ -190,11 +212,11 @@ Description This node's purpose is solely to group the categories this mod belongs to together. Children - ====================== =========== =========== - Nodes Minimum No. Maximum No. - ====================== =========== =========== - `Category`_ 0 |infin| - ====================== =========== =========== + ====================== ========== + Node Repeatable + ====================== ========== + `Category`_ **Yes** + ====================== ========== Properties *None* @@ -202,7 +224,7 @@ Properties ------------------------------------- Category -........ +-------- Tag element @@ -215,7 +237,7 @@ Children Properties =============== =============== =============================== - Properties Attributes Description + Property Attribute Description =============== =============== =============================== Text [...] A category this mod belongs to. =============== =============== =============================== @@ -223,7 +245,7 @@ Properties ------------------------------------- Version -....... +------- Tag Version @@ -236,7 +258,7 @@ Children Properties =============== =============== =============================== - Properties Attributes Description + Property Attribute Description =============== =============== =============================== Text [...] This mod's version. =============== =============== =============================== @@ -244,7 +266,7 @@ Properties ------------------------------------- Website -....... +------- Tag Website @@ -257,7 +279,7 @@ Children Properties =============== =============== =============================== - Properties Attributes Description + Property Attribute Description =============== =============== =============================== Text [...] The mod's home website. =============== =============== =============================== @@ -265,7 +287,7 @@ Properties ------------------------------------- Config -...... +------ Tag config @@ -274,17 +296,48 @@ Description The main element containing the module configuration info. Children - *None* - + =========================== ========== ================================================ + 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_desc_label: +.. _config_name_label: Name -.... +---- Tag moduleName @@ -295,13 +348,25 @@ Description 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 @@ -314,13 +379,26 @@ Description 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 @@ -332,15 +410,32 @@ Description conditions are checked. Children - *None* - + =========================== ========== + 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 @@ -351,13 +446,18 @@ Description 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 @@ -368,13 +468,18 @@ Description 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 @@ -387,13 +492,17 @@ Description Children *None* - Properties + =============== =============== ====================================== + Property Attribute Description + =============== =============== ====================================== + Version version The minimum version of the game. + =============== =============== ====================================== ------------------------------------- Installation Steps -.................. +------------------ Tag installSteps @@ -402,15 +511,30 @@ Description The list of install steps that determine which files (or plugins) that may optionally be installed for this module. Children - *None* - + =========================== ========== ============================================ + 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 @@ -419,15 +543,24 @@ Description A step in the install process containing groups of optional plugins. Children - *None* - + =========================== ========== ============================================ + 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 @@ -437,15 +570,32 @@ Description If the pattern is matched, then the install step will be visible. Children - *None* - + =========================== ========== + 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 @@ -454,15 +604,32 @@ Description A dependency that is made up of one or more dependencies. Children - *None* - + =========================== ========== + 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 @@ -471,15 +638,30 @@ Description The list of optional files (or plugins) that may optionally be installed for this module. Children - *None* - + =========================== ========== ===================================== + 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 @@ -488,15 +670,30 @@ Description A group of plugins for the mod. Children - *None* - + =========================== ========== ======================================= + 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 @@ -505,15 +702,30 @@ Description The list of plugins in the group. Children - *None* - + =========================== ========== ====================================== + 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 @@ -522,15 +734,29 @@ Description A mod plugin belonging to a group. Children - *None* - + =========================== ========== ===================================================== + 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 @@ -541,13 +767,19 @@ Description Children *None* - Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Description [...] The plugin's description. + =============== =============== =============================== ------------------------------------- +.. _plugin_image_label: + Image -..... +----- Tag image @@ -558,13 +790,17 @@ Description Children *None* - Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Path path The path to the image. + =============== =============== =============================== ------------------------------------- Files -..... +----- Tag files @@ -573,15 +809,20 @@ Description A list of files and folders to be installed. Children - *None* - + =========================== ========== + Node Repeatable + =========================== ========== + `File`_ **Yes** + `Folder`_ **Yes** + =========================== ========== Properties + *None* ------------------------------------- File -.... +---- Tag file @@ -592,13 +833,35 @@ Description 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 @@ -609,13 +872,36 @@ Description 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 @@ -624,15 +910,19 @@ Description The list of condition flags to set if the plugin is in the appropriate state. Children - *None* - + =========================== ========== ==================================== + Node Repeatable Notes + =========================== ========== ==================================== + `Flag`_ **Yes** At least one of `Flag`_ is required. + =========================== ========== ==================================== Properties + *None* ------------------------------------- Flag -.... +---- Tag flag @@ -643,13 +933,18 @@ Description Children *None* - Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Label name The flag's identifying label. + Value [...] The flag's new value. + =============== =============== =============================== ------------------------------------- Type Descriptor -............... +--------------- Tag typeDescriptor @@ -658,15 +953,20 @@ Description Describes the type of a plugin. Children - *None* - + =========================== ========== ================================================== + 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 @@ -675,15 +975,22 @@ Description Used when the plugin type is dependent upon the state of other mods. Children - *None* - + =========================== ========== =================================================== + 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 @@ -693,15 +1000,21 @@ Description The first pattern that matches the user's installation determines the type of the plugin. Children - *None* - + =========================== ========== ================================================== + Node Repeatable Notes + =========================== ========== ================================================== + :ref:`depend_pattern` **Yes** At least one of :ref:`depend_pattern` is required. + =========================== ========== ================================================== Properties + *None* ------------------------------------- +.. _depend_pattern: + Pattern -....... +------- Tag pattern @@ -710,15 +1023,20 @@ Description A specific pattern of mod files and condition flags against which to match the user's installation. Children - *None* - + =========================== ========== ============================================ + 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 @@ -729,13 +1047,22 @@ Description Children *None* - Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Type name Describes the plugin's type. + + Accepts ``Required``, + ``Recommended``, ``Optional``, + ``CouldBeUsable`` or + ``NotUsable`` + =============== =============== =============================== ------------------------------------- Default Type -............ +------------ Tag defaultType @@ -746,13 +1073,22 @@ Description Children *None* - Properties + =============== =============== =============================== + Property Attribute Description + =============== =============== =============================== + Type name Describes the plugin's type. + + Accepts ``Required``, + ``Recommended``, ``Optional``, + ``CouldBeUsable`` or + ``NotUsable`` + =============== =============== =============================== ------------------------------------- Mod Requirements -................ +---------------- Tag requiredInstallFiles @@ -761,15 +1097,20 @@ Description The list of files and folders that must be installed for this module. Children - *None* - + =========================== ========== + Node Repeatable + =========================== ========== + `File`_ **Yes** + `Folder`_ **Yes** + =========================== ========== Properties + *None* ------------------------------------- Conditional Installation -........................ +------------------------ Tag conditionalFileInstalls @@ -778,15 +1119,21 @@ Description The list of optional files that may optionally be installed for this module, based on condition flags. Children - *None* - + =========================== ========== ================================================= + Node Repeatable Notes + =========================== ========== ================================================= + :ref:`cond_patterns` **No** At least one of :ref:`cond_patterns` is required. + =========================== ========== ================================================= Properties + *None* ------------------------------------- +.. _cond_patterns: + Patterns -........ +-------- Tag patterns @@ -796,15 +1143,21 @@ Description All matching patterns will have their files installed. Children - *None* - + =========================== ========== ================================================ + Node Repeatable Notes + =========================== ========== ================================================ + :ref:`cond_pattern` **Yes** At least one of :ref:`cond_pattern` is required. + =========================== ========== ================================================ Properties + *None* ------------------------------------- +.. _cond_pattern: + Pattern -....... +------- Tag pattern @@ -813,8 +1166,12 @@ Description A specific pattern of mod files and condition flags against which to match the user's installation. Children - *None* - + =========================== ========== ============================================ + Node Repeatable Notes + =========================== ========== ============================================ + `Files`_ **No** At least one of `Files`_ is required. + `Dependencies`_ **No** At least one of `Dependencies`_ is required. + =========================== ========== ============================================ Properties - + *None* From fe13de6aba7a34d3678f7fdfbd03d95536d00601 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 2 Aug 2016 21:08:15 +0000 Subject: [PATCH 19/31] Added note to getting started document. --- docs/source/getting_started.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 6c8bc83..66c3937 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -6,7 +6,7 @@ Welcome to the *FOMOD Designer*! 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, +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. @@ -16,6 +16,10 @@ is located. **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 :doc:`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 +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. From 247904a2be118e2420828ac56a4118a5954409bf Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 6 Aug 2016 19:42:30 +0000 Subject: [PATCH 20/31] Users should now be able to hide nodes. --- designer/gui.py | 52 +++++++++++++--- designer/io.py | 43 ++++++++----- designer/nodes.py | 72 ++++++++++++++++------ designer/previews.py | 4 +- designer/ui_templates/window_mainframe.py | 21 +++++-- designer/wizards.py | 20 +++--- resources/logos/logo_hide.png | Bin 0 -> 2259 bytes resources/logos/logo_show.png | Bin 0 -> 1995 bytes resources/templates/window_mainframe.ui | 35 ++++++++--- tests/test_io_nodes_props.py | 10 +-- 10 files changed, 186 insertions(+), 71 deletions(-) create mode 100644 resources/logos/logo_hide.png create mode 100644 resources/logos/logo_show.png diff --git a/designer/gui.py b/designer/gui.py index d006e2e..f743d0a 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -35,7 +35,8 @@ from requests import get, 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 .nodes import _NodeBase +from .io import import_, new, export, sort_nodes, node_factory, copy_node from .previews import PreviewDispatcherThread from .props import PropertyFile, PropertyColour, PropertyFolder, PropertyCombo, PropertyInt, PropertyText, \ PropertyFlagLabel, PropertyFlagValue, PropertyHTML @@ -201,7 +202,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])) @@ -373,7 +374,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( @@ -401,7 +402,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) @@ -433,6 +434,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) @@ -460,6 +463,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) @@ -521,7 +526,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: _NodeBase self.select_node.connect( lambda index: self.set_current_node(self.node_tree_model.itemFromIndex(index).xml_node) ) @@ -537,6 +542,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) @@ -566,6 +587,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() @@ -597,7 +623,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: @@ -674,6 +700,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) @@ -758,8 +792,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) + sort_nodes(self._info_root) + sort_nodes(self._config_root) if self.settings_dict["Save"]["validate"]: try: validate_tree( @@ -811,6 +845,8 @@ def delete(self): if self.current_node is None: self.statusBar().showMessage("Can't delete nothing.") 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, diff --git a/designer/io.py b/designer/io.py index f2a4d3e..2ec4675 100644 --- a/designer/io.py +++ b/designer/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 @@ -176,7 +176,7 @@ def _validate_child(child): return False -def elem_factory(tag, parent=None): +def node_factory(tag, parent=None): """ Function meant as a replacement for the default element factory. @@ -208,18 +208,19 @@ def elem_factory(tag, parent=None): return module_parser.makeelement(tag) -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)) +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 @@ -287,7 +288,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: @@ -317,14 +327,17 @@ def export(info_root, config_root, package_path): config_tree = ElementTree(config_root) config_tree.write(configfile, pretty_print=True) + for pair in hidden_nodes_pairs: + pair[0].insert(pair[2], pair[1]) + -def sort_elements(root_element): +def sort_nodes(root_node): """ Sorts the xml elements according to their sort_order member. - :param root_element: The root element of xml tree. + :param root_node: The root element of xml tree. """ - for parent in root_element.xpath('//*[./*]'): + for parent in root_node.xpath('//*[./*]'): parent[:] = sorted( parent, key=lambda x: x.sort_order + "." + x.user_sort_order if not isinstance(x, CommentBase) else "0" diff --git a/designer/nodes.py b/designer/nodes.py index bd159e3..83eb5c5 100644 --- a/designer/nodes.py +++ b/designer/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, sort_nodes from .wizards import WizardFiles, WizardDepend from .props import PropertyCombo, PropertyInt, PropertyText, PropertyFile, PropertyFolder, PropertyColour, \ PropertyFlagLabel, PropertyFlagValue, PropertyHTML @@ -40,20 +41,16 @@ def _init(self): 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,6 +71,8 @@ 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 = {} @@ -127,6 +126,16 @@ 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 parse_attribs(self): """ Reads the values from the BaseElement's attrib dictionary into the node's properties. @@ -150,6 +159,8 @@ 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): """ @@ -173,6 +184,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: _NodeBase + self.add_child(node) if node.tag is not etree.Comment else self.append(node) + node.set_hidden(True) + sort_nodes(self) + self.model_item.sortChildren(0) def save_metadata(self): """ @@ -182,23 +202,37 @@ def save_metadata(self): self.metadata["name"] = self.model_item.text() else: self.metadata.pop("name", None) - if 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 type(child) is NodeComment: if child.text.split()[0] == "": 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: + if meta_comment is None and self.metadata: self.append(NodeComment(" " + encode(self.metadata))) diff --git a/designer/previews.py b/designer/previews.py index 6b3fdc3..74f11ba 100644 --- a/designer/previews.py +++ b/designer/previews.py @@ -22,7 +22,7 @@ from pygments import highlight from pygments.formatters.html import HtmlFormatter from pygments.lexers.html import XmlLexer -from .io import sort_elements +from .io import sort_nodes class PreviewDispatcherThread(QThread): @@ -53,7 +53,7 @@ def run(self): if element is not None: element.write_attribs() element.load_metadata() - sort_elements(element) + sort_nodes(element) # dispatch to every queue self.gui_queue.put(element) diff --git a/designer/ui_templates/window_mainframe.py b/designer/ui_templates/window_mainframe.py index ca8839d..b2e189d 100644 --- a/designer/ui_templates/window_mainframe.py +++ b/designer/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/wizards.py b/designer/wizards.py index 4f510ae..8db8058 100644 --- a/designer/wizards.py +++ b/designer/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/resources/logos/logo_hide.png b/resources/logos/logo_hide.png new file mode 100644 index 0000000000000000000000000000000000000000..d1dd238408f7c1a7c7b3880eeb7174d313fa3b43 GIT binary patch literal 2259 zcmeH{iC2@y8pgAb0713_B_ag@0oh^#2^u64Ajp!GvII;BP*NKeDY8T$2ue_I%dQk6 zR0?Rs1qgyID62%!fW&geE0M(|jUXb6pn%9`C*GcW?)@L`bIv^Td*6BIn>pXind|_6 zZ%vdD3Ic&>`ub3UKu=aSH8}9{Tba|KgL6H6>1t|fqkn!Q2cder50eXlz}A!vT4h~@ z1;|?56VJC7RDJdzK=p!-Q{p@%SCNA+{*{rJ(sD2$lqkDb&^a<4eU;p_7 zdb_>#NmK*3uMIte4ZY|jH?kYqo$T)E?n$9|d3*a(wJ}5;M-tkXiS`S_U~yQSBhC@; zh$lD_obd!_!Y-ooE~4Mp{xtspOYL zH(A(jva~meNHmS)Mt-;7)C|bn%-mxC!9$!wF(#&_U{}V*CO^b;j?etJHQ!dQ<=QL<-qP6(yj6TxnQ2wD z-MI&#{j-zW#@f4-n<2H2cXaoBvNq2G!Q#;%|6m9VfmBsfM`>#7e4&rtY_!G1%-qVx z)&b|}gm>P#i?qkh!xNmfFF3_-0)sgwuN>aJ4vBoawaZ*TAYzP>)G^uZtf{SO~L z902;)W2FXVvL{1B!^0yZBcr3EPoF(|{`|!Yx%^LsLh*8JZ2Z;4#Ov3S(?Byov$JpC z&cA#2{`~^b;?jrZ<&Pg%R#sQn*4Eb7H#Rojj&SAR3WWPo-0A4eGk4O`1zcnCe4MQx z)2xLSbg?+RLKYj!HS`M0vZ997jXmGnn|wv>euoz(n96pr^6)J4GPJiE4<(+zH~Gue zgU3&FW~`r>m>%p8t&1GZ=s4Cn-r(s+OWz%xQ;PB^z`MK3T^{jfJor;SFNr2fKJF?` zs+v~--zH4z!F23sU#QXTu5a(1l-~M0hAjS{}_LiHkautV!bpP$up}LkuvOuY6>d9$o z;J&uUiB9`!w~tCxs>dT_w57zS>GpB2tQq%fDozIFX;)bOKxb7?JTPsD40q1Ox?Ol< z!#a(NnYUxq`WLwrh!i8kqtJsIfjO13UeA9qI zq75_nca2&)#a*XVv^cU8XWJ$Hn z+Q+XK3?-LF(5m$7p|*0pdu7d@%uyNiWz#%Xa;|9G^lJgSdL#@*3)m~R9-$aGtMgc_ zTkP~9KT#n%<#6p&Rlj0EfuQnisXnYdoFjnD9*8CJ6Ux!Yu_iC->W0O#!yLLHyQRQq z8g-%Bk15TZXl$B_&f|K8rE8@x)N4@@?}`PUr>C;+{>b;>aG)Qi8amw&q4*Ma{w;<# z4eBTmz{cA6Q(4=f!+n0pS($GADp_a`v6?b+;^V;t^#YCNf^D#1fdKm8G2seag9DBK z={gSH8z7@^4Ky~4FIU-0Wk!oPMeKKJ2tdMT+gucwtxV=H`RA;cup@iU3PrT2oC2Oy z73rHBB2N1>*|5sD=NP~)GIV<^&Apka=;t!p6F+G$s+fR_I2L<9?b&LIz=qVkGrf`%DaZXs)!W0z9mAN(g)Dv8edh!v=Fu zw_(JU==JfSyr_fOjt{$+UlBT%c3xfL_NwdS=tTapdb@40Nd+7nTy>0NK zN_tlXZ<-|%SW`(oxB264Eh?89$%93CO|QP{+S^pvfj9LlmY2rb_PwE%gt0J3tX3aQ z9uSJk=tA=vsuleU4kc~4@(4HUrmG#%!oexdZs7K;z2+pL=PLU-B$`26%XeR@{%!%Q zJ-2c{vvC@S&(KIZUD4VeLoI}w$QK2t25LM@-m89#OTT-Tid=G?S+=kgcB=9c9`V*) z#ZwmfPoBVCy^VJC!#!p$&#&;v?f6VRp#mM9lRS@09J)A`;b`WqrKZ?C4jx*Fua`gN Jx<}N>zW~%ITnhjI literal 0 HcmV?d00001 diff --git a/resources/logos/logo_show.png b/resources/logos/logo_show.png new file mode 100644 index 0000000000000000000000000000000000000000..e859fcef7cd904deb339cf030f44602e17df0bb0 GIT binary patch literal 1995 zcmd^<|36cE9LIMy+s@|8X1?ACg%Ec}xKuZrXolAErAF8&^5x!)DY@5mx3NofCEP?` zxTV4!jZ6`;s1!+c<3=&$-fl$^MZVq7+{feI`#;?0<9uG9*XwoO=aeZfvuE*J&$%vIqVu%Y;F41XAg2ZnbjK*aGp1NjI9azVMEGK(?_C_hW| zU?=*;MJ6VNB}5>Sl9I^L2V(hq!*~(oxP<+uCN{4E(?cvDe~;zmWibCg1Z!u7b+CaQ zKZji?U%FCVsWjSFcXtoE2i=p-@CFBhwl>9WaUfIdD0cP~dwY9FCkODjxHvjFJ36~~ zZD;y0S!i=3+_sH2pOeXCO2F>GfWRQ|p@oH&h1L2!A)FBIUJzA`R;jVJnwpv$uK}#x zyFV%-D%Qx@I4bslmX400p^>4H(N{cv9RCm)*4NkPB__omJj_o@*45JkjgF4q!Q>Pz zZEd2aCXq-?`yneWGdumrQSeGlU7Z*onvtCYeAbW0fkJpJURBA_+D9)SDX8{e(*zow!c?Ue34s2?wU(Q}>z)W zwz_YlGrX8PcKQXdgZ2c6aKiRQMDyYk_}^z`AI~c&EIxC-th}O9QYEdC)n99BzVQ#O z|3$vSO(kuBcEGJ$w*hxLl-#{{?_THq?gu?R4*`!K|ElE4liuFmzP`TSo+|0@9~c;T z_Wb$a;LwmlF#;HU@%tYy0k2=b0gR82PfWah`|jQ3KYsH0 zF-ye<e4-YM6>IBa?yjWKrB2$dhH!A{4H;0bb)>z)_D@5<#k1edCYpUh8 zR)lmB0*8lZn@dW$aSLH)M?QwGWy>}^ZKnGkbe$fwFyq-yZ^?VH$6%Y!;=%>usKE(Q z`J%P!&%&SPX(O>gKQAe0eU}fzHV>OCHQeuSoWY?^7o+nkiUtU~u6bhuZalQkeR&+m z$qB_gJ?7!sqmpY-y?WshA5-?B$f-7UwREXvl+$7+Tp;k5U(3b<1IJ=HZ7C2wBi)vx z!GtPO&*k)7UQ*GvAAZ}C;-c!Sg?vIe_(gl$^-FGOH`>LL`Khspjh0Qu>*dqlt_UmZ zIi$*Q>g~ImxKDPBDj2W6e&**HEC}C#FC!b9rR+95SHd^Q)Ke!d+Z%V_)7W|$r!7ZJ z!Y8S}8%l4&xi2?rNTE=ZLuB?WspC-<@nhOa+gE;muQ`Heh{;=_te6&+UB#$JW0*&A zaJXYCBD)?cGFUT=omxICE)jBvjabf9H&Q|6fZS`yQ84`{^2&sUN%Nc?`1d)7wr%>|g0gFX=QIIB16H2tp zM;N8$((scS1cXUBGND$!{)#~2e-f&oGIiO`1|5~63NkVwQl##gjtj{9s&zBQo7GAd z;mz|$#L*_U>J4Qqj5i)f& zbepL@ZjDHS;8?e!phHGz?>2R^dVsN3!yaKqrtQNt&`mZKXLk9N`n+#Knx%xoCsxa- z)hU`NpYBVz$Um;@wWKr*Vo5D)#FoTq`H7jci&N&pOk~77hF4>q5o}f@+U`u5?3ACH z&92L)$6XL+l^i4Zvs67~THL-v)zxxN`7S+UlyxvUE7V3ck(M>nKR25-gR(Y}k8}$I z?WE4%nbn6UN98ofDTb{{w4+8@|DfR+Y$t21ly~7tL|?#M!Qtx1&+q z!}>hUKF_enyr3U?+mq{_34dFs@r`L`w-;w;I=}9r3w|3^1tAJ?<9^~UZAWm^AQ&D@ J_i8t;=r1=H&a(gj literal 0 HcmV?d00001 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/tests/test_io_nodes_props.py b/tests/test_io_nodes_props.py index 6505482..27eebca 100644 --- a/tests/test_io_nodes_props.py +++ b/tests/test_io_nodes_props.py @@ -16,7 +16,7 @@ import sys, os, lxml, pytest sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from designer.io import import_, export, module_parser, sort_elements, new, copy_element, elem_factory +from designer.io import import_, export, module_parser, sort_nodes, new, copy_node, node_factory from designer.exceptions import TagNotFound, ParserError, BaseInstanceException from designer.nodes import _NodeBase from designer.props import _PropertyBase @@ -26,8 +26,8 @@ def test_import_export(tmpdir): tmpdir = str(tmpdir) info_root, config_root = import_(os.path.join(os.path.dirname(__file__), "data", "valid_fomod")) - sort_elements(info_root) - sort_elements(config_root) + sort_nodes(info_root) + sort_nodes(config_root) export(info_root, config_root, tmpdir) with open(os.path.join(os.path.dirname(__file__), "data", "valid_fomod", "fomod", "Info.xml")) as info_base: @@ -77,9 +77,9 @@ def test_node_operations(): assert base_info == lxml.etree.tostring(info_root, encoding="unicode") assert base_config == lxml.etree.tostring(config_root, encoding="unicode") - new_elem = elem_factory(config_root.allowed_children[0].tag, config_root) + new_elem = node_factory(config_root.allowed_children[0].tag, config_root) config_root.add_child(new_elem) - new_config_root = copy_element(config_root) + 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") From 3f615fb476275fd7aef0010d50a9c1b2ed295d91 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Mon, 8 Aug 2016 03:16:57 +0000 Subject: [PATCH 21/31] Updated testing procedures. --- .travis.yml | 10 ++++++++-- appveyor.yml | 2 +- dev/appveyor-bootstrap.bat | 1 + dev/reqs.txt | 2 -- dev/test-reqs.txt | 4 ++++ dev/travis-bootstrap.sh | 1 + dev/travis-test.sh | 4 +++- tasks.py | 15 --------------- 8 files changed, 18 insertions(+), 21 deletions(-) create mode 100644 dev/test-reqs.txt diff --git a/.travis.yml b/.travis.yml index 85f4c05..45c6413 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,17 +6,23 @@ branches: - master - develop +virtualenv: + system_site_packages: true + install: - chmod +x ./dev/travis-bootstrap.sh - ./dev/travis-bootstrap.sh - - pip install coveralls + +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: - - coveralls - chmod +x ./dev/travis-build.sh - ./dev/travis-build.sh diff --git a/appveyor.yml b/appveyor.yml index 7f5befb..2ad36f6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ install: build: off test_script: - - inv test + - py.test --cov=designer --cov-report html --cov-report term -vv tests/ after_test: - inv build diff --git a/dev/appveyor-bootstrap.bat b/dev/appveyor-bootstrap.bat index 4c4691c..20d668d 100644 --- a/dev/appveyor-bootstrap.bat +++ b/dev/appveyor-bootstrap.bat @@ -10,3 +10,4 @@ call activate 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/reqs.txt b/dev/reqs.txt index 7c97402..3c0b60b 100644 --- a/dev/reqs.txt +++ b/dev/reqs.txt @@ -7,6 +7,4 @@ lxml==3.5.0 py==1.4.31 Pygments==2.1.3 PyInstaller==3.1.1 -pytest==2.9.2 -pytest-cov==2.3.0 requests==2.10.0 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 index b510cf6..c7a26c3 100644 --- a/dev/travis-test.sh +++ b/dev/travis-test.sh @@ -21,4 +21,6 @@ eval "$(pyenv virtualenv-init -)" pyenv shell miniconda3-3.19.0/envs/fomod-designer -invoke test +py.test --cov=designer --cov-report html --cov-report term -vv tests/ + +coveralls diff --git a/tasks.py b/tasks.py index f88b3a3..475cbcc 100644 --- a/tasks.py +++ b/tasks.py @@ -61,21 +61,6 @@ def clean(): print("Build caches cleaned.") -@task -def clean_test(): - from shutil import rmtree - - rmtree("htmlcov", ignore_errors=True) - print("Test caches cleaned.") - - -@task(clean_test) -def test(): - from os import system - - system("py.test --cov=designer --cov-report html --cov-report term -vv tests/") - - @task(clean) def build(): from platform import system, architecture From 97210d9790f2fbb57c4e177a50cb6587a71999c9 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Mon, 8 Aug 2016 23:38:12 +0000 Subject: [PATCH 22/31] Removed code from documentation. --- .gitignore | 1 - docs/source/api.rst | 15 --------------- docs/source/index.rst | 1 - tasks.py | 2 -- 4 files changed, 19 deletions(-) delete mode 100644 docs/source/api.rst diff --git a/.gitignore b/.gitignore index acb4113..9857943 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,6 @@ coverage.xml # Sphinx documentation docs/build/ -docs/source/api resources/docs # PyBuilder diff --git a/docs/source/api.rst b/docs/source/api.rst deleted file mode 100644 index 181993b..0000000 --- a/docs/source/api.rst +++ /dev/null @@ -1,15 +0,0 @@ -Code Documentation -================== - -Want to contribute? Want to learn more about what is happening behind the scenes? Then this is the place for you! -Take a look through the code documentation to familiarize yourself with the code before you dive in the repository. -Contributions and help are always welcome! - -This is a good place to start when you're -thinking of contributing or using this for a project of your own. Below you can find the complete -table of contents with all the project modules: - -.. toctree:: - :glob: - - api/* diff --git a/docs/source/index.rst b/docs/source/index.rst index c9cf371..12305ca 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,5 +13,4 @@ Installation Basic Usage Advanced Usage - Code Documentation F.A.Q. diff --git a/tasks.py b/tasks.py index b4df7b4..d7c89c9 100644 --- a/tasks.py +++ b/tasks.py @@ -78,8 +78,6 @@ def docs(): from os.path import join makedirs(join("resources", "docs"), exist_ok=True) - system("sphinx-apidoc -TPMeo {} {} {}".format(join("docs", "source", "api"), "designer", - join("designer", "ui_templates"))) system("sphinx-build -b html -d {} {} {}".format(join("docs", "build", "doctrees"), join("docs", "source"), join("resources", "docs"))) From 21ebc875931f940bdf1e2fc4c2f9db8e069edfa9 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Mon, 8 Aug 2016 23:39:11 +0000 Subject: [PATCH 23/31] Fixed links. --- docs/source/advanced.rst | 2 +- docs/source/faq.rst | 2 ++ docs/source/getting_started.rst | 2 +- docs/source/install.rst | 6 +++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 84b798f..15c2596 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -82,7 +82,7 @@ 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 <>`_. +feel free to look at the `revised *FOMOD* schema `_. Info ---- diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 73c3848..8ddc0d1 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -1,6 +1,8 @@ Frequently Asked Questions ========================== +.. _open_new_merged: + Why are the *New* and *Open* buttons merged? ++++++++++++++++++++++++++++++++++++++++++++ diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 66c3937..61de350 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -14,7 +14,7 @@ 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 :doc:`F.A.Q. `. + 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* diff --git a/docs/source/install.rst b/docs/source/install.rst index 3fb4c1b..5c9cbe3 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -12,7 +12,7 @@ There are pre-built, ready-to-use executables always available for 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 <>`_. +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. @@ -20,11 +20,11 @@ of work try building from source. Building from Source ++++++++++++++++++++ -1. Download the `repository from Github <>`_; +1. Download the `repository from Github `_; 2. Unpack the archive into a folder; -3. Install `Conda <>`_; +3. Install `Conda `_; 4. Open the command line/terminal in the folder from step 2; From c024bb316940a680018fa743eb5d9e62b23edd84 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Mon, 8 Aug 2016 23:45:02 +0000 Subject: [PATCH 24/31] Fixed index toc tree. --- docs/source/getting_started.rst | 25 ------------------------- docs/source/index.rst | 28 ++++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 29 deletions(-) delete mode 100644 docs/source/getting_started.rst diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst deleted file mode 100644 index 61de350..0000000 --- a/docs/source/getting_started.rst +++ /dev/null @@ -1,25 +0,0 @@ -Getting Started -=============== - -Welcome to the *FOMOD Designer*! 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. diff --git a/docs/source/index.rst b/docs/source/index.rst index 12305ca..964f775 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,15 +1,35 @@ -.. FOMOD Designer documentation master file, created by - sphinx-quickstart on Thu Jun 9 19:41:15 2016. +Getting Started +=============== -.. include:: getting_started.rst +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 + Getting Started Installation Basic Usage Advanced Usage From f4700f514e0b537c54a2ba2649af989850915c0f Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Tue, 9 Aug 2016 00:53:46 +0000 Subject: [PATCH 25/31] Added contributing and changelog to the docs. General docs cleanup. --- CHANGELOG.md => CHANGELOG.rst | 31 +++++++++++------------ CONTRIBUTING.md | 38 ----------------------------- CONTRIBUTING.rst | 46 +++++++++++++++++++++++++++++++++++ README.md | 16 ------------ appveyor.yml | 2 +- docs/source/basic.rst | 31 +++++------------------ docs/source/changelog.rst | 1 + docs/source/contributing.rst | 1 + docs/source/index.rst | 2 ++ tasks.py | 15 ++---------- 10 files changed, 75 insertions(+), 108 deletions(-) rename CHANGELOG.md => CHANGELOG.rst (93%) delete mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTING.rst create mode 100644 docs/source/changelog.rst create mode 100644 docs/source/contributing.rst diff --git a/CHANGELOG.md b/CHANGELOG.rst similarity index 93% rename from CHANGELOG.md rename to CHANGELOG.rst index 317d839..3b17550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.rst @@ -1,6 +1,7 @@ -# Changelog +Changelog +========= -0.7.2 (2016-07-13) +**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 +9,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 +49,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 +58,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 +80,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 +107,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 +121,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 +133,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..c5910eb --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,46 @@ +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 ++++++++++++++++++ + +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 + + +Fork the repo. + +Make your change and make sure the tests pass on the CI. Follow the `style guide `_. + +.. todo:: + Integrate tox with vagrant/conda/pyenv + +Push to your fork. Write a `good commit message `_. Submit a pull request. + +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. + +Thank you, `contributors `_! diff --git a/README.md b/README.md index 145680f..d4030c2 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,6 @@ You can get more information in the official documentation hosted at [Read The Docs](http://fomod-designer.readthedocs.io/en/stable/). -## 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 - ## License ***FOMOD Designer*** is free software and may be redistributed diff --git a/appveyor.yml b/appveyor.yml index 2ad36f6..61b2cc7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,7 +23,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.](https://github.com/GandaG/fomod-designer/blob/master/CHANGELOG.rst)' force_update: true on: appveyor_repo_tag: true diff --git a/docs/source/basic.rst b/docs/source/basic.rst index 6feda2b..db33637 100644 --- a/docs/source/basic.rst +++ b/docs/source/basic.rst @@ -1,7 +1,8 @@ Basic Usage =========== -.. Describe basic usage - basic view and wizards +.. 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. @@ -11,7 +12,8 @@ If you're mid-way through your work but you want to save and leave, simply hit ` Basic View ++++++++++ -.. Describe basic view here - pretty much just the initial page +.. todo:: + Describe basic view here - pretty much just the initial page. Wizards +++++++ @@ -19,26 +21,5 @@ 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. -Info ----- - -Config ------- - -Installation Steps ------------------- - -Installation Step ------------------ - -Plugin Group ------------- - -Plugin ------- - -Dependencies ------------- - -Files ------ +.. 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/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/index.rst b/docs/source/index.rst index 964f775..c6ee98a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -33,4 +33,6 @@ head to the :doc:`Advanced Usage `. Installation Basic Usage Advanced Usage + Contributing + Changelog F.A.Q. diff --git a/tasks.py b/tasks.py index d7c89c9..315caff 100644 --- a/tasks.py +++ b/tasks.py @@ -52,17 +52,6 @@ def preview(): run("python dev/pyinstaller-bootstrap.py") -@task -def clean_docs(): - from shutil import rmtree - from os.path import join - - rmtree(join("docs", "build"), ignore_errors=True) - rmtree(join("resources", "docs"), ignore_errors=True) - rmtree(join("docs", "source", "api"), ignore_errors=True) - print("Documentation caches cleaned.") - - @task() def clean(): from shutil import rmtree @@ -72,7 +61,7 @@ def clean(): print("Build caches cleaned.") -@task(clean_docs) +@task() def docs(): from os import system, makedirs from os.path import join @@ -93,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", "README.md", "CHANGELOG.rst", "CONTRIBUTING.rst"] archive_name = "designer" # the archive's name try: From e2f6c647bbce197428ff773ace149710e9943337 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Tue, 9 Aug 2016 02:12:36 +0000 Subject: [PATCH 26/31] Added getting started button to intro window. Added functionality to the help action. --- README.md | 2 +- appveyor.yml | 2 +- designer/gui.py | 26 +++++++------ designer/ui_templates/window_intro.py | 38 ++++++++++++------ resources/templates/window_intro.ui | 55 +++++++++++++++++++++++++++ tests/test_gui.py | 23 ++++++++++- 6 files changed, 119 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d4030c2..02f63da 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # FOMOD Designer -[![Build status](https://ci.appveyor.com/api/projects/status/nep4id3ammekof68?svg=true)](https://ci.appveyor.com/project/GandaG/fomod-editor) [![Build Status](https://travis-ci.org/GandaG/fomod-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) +[![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.* diff --git a/appveyor.yml b/appveyor.yml index 61b2cc7..effbd43 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,7 +23,7 @@ deploy: auth_token: secure: iMaZrvVT+OI/9jRs8LyOvmzVqIBa0/jpiK96wNzZww/KqKsMcferhIeSK7faNzOo artifact: windows_build - description: '[Changelog.](https://github.com/GandaG/fomod-designer/blob/master/CHANGELOG.rst)' + description: '[Changelog.](http://fomod-designer.readthedocs.io/en/stable/changelog.html)' force_update: true on: appveyor_repo_tag: true diff --git a/designer/gui.py b/designer/gui.py index f743d0a..876af42 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -15,11 +15,11 @@ # limitations under the License. from os import makedirs, listdir -from os.path import expanduser, normpath, basename, join, relpath, isdir, isfile +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 @@ -32,7 +32,7 @@ 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 .nodes import _NodeBase @@ -76,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): @@ -666,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(): @@ -857,7 +858,15 @@ def delete(self): @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): @@ -2149,13 +2158,6 @@ def key(self): } -def not_implemented(): - """ - A convenience function for something that has not yet been implemented. - """ - return generic_errorbox("Nope", "Sorry, this part hasn't been implemented yet!") - - def generic_errorbox(title, text, detail=""): """ A function that creates a generic errorbox with the logo_admin.png logo. diff --git a/designer/ui_templates/window_intro.py b/designer/ui_templates/window_intro.py index a6c76b1..8391225 100644 --- a/designer/ui_templates/window_intro.py +++ b/designer/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/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/tests/test_gui.py b/tests/test_gui.py index 9615ee7..5775787 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -19,8 +19,9 @@ from datetime import datetime from copy import deepcopy from io import StringIO -from unittest.mock import patch +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 @@ -46,6 +47,26 @@ def test_about_dialog(qtbot): assert not about_window.isVisible() +@patch('designer.gui.open_new_tab') +@patch('designer.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() From 778ad72c5aaff5004bb3dbd0fe97f4397c57f028 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Tue, 9 Aug 2016 04:39:31 +0000 Subject: [PATCH 27/31] Moved node sorting method. --- designer/gui.py | 6 +++--- designer/io.py | 13 ------------- designer/nodes.py | 11 +++++++++-- designer/previews.py | 3 +-- tests/test_io_nodes_props.py | 6 +++--- 5 files changed, 16 insertions(+), 23 deletions(-) diff --git a/designer/gui.py b/designer/gui.py index 876af42..4d312c1 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -36,7 +36,7 @@ from validator import validate_tree, check_warnings, ValidatorError, ValidationError, WarningError, MissingFolderError from . import cur_folder, __version__ from .nodes import _NodeBase -from .io import import_, new, export, sort_nodes, node_factory, copy_node +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 @@ -793,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_nodes(self._info_root) - sort_nodes(self._config_root) + self._info_root.sort() + self._config_root.sort() if self.settings_dict["Save"]["validate"]: try: validate_tree( diff --git a/designer/io.py b/designer/io.py index 2ec4675..9baf23a 100644 --- a/designer/io.py +++ b/designer/io.py @@ -329,16 +329,3 @@ def export(info_root, config_root, package_path): for pair in hidden_nodes_pairs: pair[0].insert(pair[2], pair[1]) - - -def sort_nodes(root_node): - """ - Sorts the xml elements according to their sort_order member. - - :param root_node: The root element of xml tree. - """ - for parent in root_node.xpath('//*[./*]'): - parent[:] = sorted( - parent, - key=lambda x: x.sort_order + "." + x.user_sort_order if not isinstance(x, CommentBase) else "0" - ) diff --git a/designer/nodes.py b/designer/nodes.py index 83eb5c5..c70cd71 100644 --- a/designer/nodes.py +++ b/designer/nodes.py @@ -21,7 +21,7 @@ from lxml import etree, objectify from jsonpickle import encode, decode, set_encoder_options from json import JSONDecodeError -from .io import copy_node, sort_nodes +from .io import copy_node from .wizards import WizardFiles, WizardDepend from .props import PropertyCombo, PropertyInt, PropertyText, PropertyFile, PropertyFolder, PropertyColour, \ PropertyFlagLabel, PropertyFlagValue, PropertyHTML @@ -136,6 +136,13 @@ def set_hidden(self, hide: bool): 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 if x.tag is not etree.Comment else "0" + ) + def parse_attribs(self): """ Reads the values from the BaseElement's attrib dictionary into the node's properties. @@ -191,7 +198,7 @@ def load_metadata(self): node = copy_node(etree.fromstring(node_string), self) # type: _NodeBase self.add_child(node) if node.tag is not etree.Comment else self.append(node) node.set_hidden(True) - sort_nodes(self) + self.sort() self.model_item.sortChildren(0) def save_metadata(self): diff --git a/designer/previews.py b/designer/previews.py index 74f11ba..6fa1257 100644 --- a/designer/previews.py +++ b/designer/previews.py @@ -22,7 +22,6 @@ from pygments import highlight from pygments.formatters.html import HtmlFormatter from pygments.lexers.html import XmlLexer -from .io import sort_nodes class PreviewDispatcherThread(QThread): @@ -53,7 +52,7 @@ def run(self): if element is not None: element.write_attribs() element.load_metadata() - sort_nodes(element) + element.sort() # dispatch to every queue self.gui_queue.put(element) diff --git a/tests/test_io_nodes_props.py b/tests/test_io_nodes_props.py index 27eebca..6485512 100644 --- a/tests/test_io_nodes_props.py +++ b/tests/test_io_nodes_props.py @@ -16,7 +16,7 @@ import sys, os, lxml, pytest sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from designer.io import import_, export, module_parser, sort_nodes, new, copy_node, node_factory +from designer.io import import_, export, module_parser, new, copy_node, node_factory from designer.exceptions import TagNotFound, ParserError, BaseInstanceException from designer.nodes import _NodeBase from designer.props import _PropertyBase @@ -26,8 +26,8 @@ def test_import_export(tmpdir): tmpdir = str(tmpdir) info_root, config_root = import_(os.path.join(os.path.dirname(__file__), "data", "valid_fomod")) - sort_nodes(info_root) - sort_nodes(config_root) + 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: From 507d7ce5f8412346c3cee26a906e0ad660ec45fd Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Wed, 10 Aug 2016 05:05:22 +0000 Subject: [PATCH 28/31] Closes #42. Users should now be able to add comment nodes. --- designer/gui.py | 56 ++++++++----- designer/io.py | 18 +++-- designer/nodes.py | 148 +++++++++++++++++++++++------------ designer/previews.py | 4 +- tasks.py | 2 +- tests/test_io_nodes_props.py | 4 +- 6 files changed, 150 insertions(+), 82 deletions(-) diff --git a/designer/gui.py b/designer/gui.py index 4d312c1..5743be9 100644 --- a/designer/gui.py +++ b/designer/gui.py @@ -24,7 +24,7 @@ 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, QHeaderView, @@ -35,7 +35,7 @@ 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 .nodes import _NodeBase +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, \ @@ -527,7 +527,7 @@ def __init__(self): self.flag_value_completer.setModel(self.flag_value_model) # connect node selected signal - self.current_node = None # type: _NodeBase + self.current_node = None # type: _NodeElement self.select_node.connect( lambda index: self.set_current_node(self.node_tree_model.itemFromIndex(index).xml_node) ) @@ -842,19 +842,18 @@ 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: - 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 - )) - 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(): @@ -939,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() @@ -1020,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) @@ -1041,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) @@ -1065,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_): diff --git a/designer/io.py b/designer/io.py index 9baf23a..0ff4fc6 100644 --- a/designer/io.py +++ b/designer/io.py @@ -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(): @@ -188,7 +188,10 @@ def node_factory(tag, parent=None): :param parent: The parent of the future element. :return: The created element with the tag *tag*. """ - if parent is not None: + 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) @@ -249,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() diff --git a/designer/nodes.py b/designer/nodes.py index c70cd71..96f583b 100644 --- a/designer/nodes.py +++ b/designer/nodes.py @@ -29,15 +29,57 @@ 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 + + def sort(self): + pass -class _NodeBase(etree.ElementBase): +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() @@ -76,7 +118,7 @@ def init(self, name, tag, allowed_instances, 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) @@ -100,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 @@ -140,7 +182,7 @@ def sort(self): for parent in self.xpath('//*[./*]'): parent[:] = sorted( parent, - key=lambda x: x.sort_order + "." + x.user_sort_order if x.tag is not etree.Comment else "0" + key=lambda x: x.sort_order + "." + x.user_sort_order ) def parse_attribs(self): @@ -183,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: @@ -195,7 +237,7 @@ def load_metadata(self): 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: _NodeBase + 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() @@ -232,7 +274,7 @@ def save_metadata(self): set_encoder_options("json", separators=(',', ':')) for child in self: if type(child) is NodeComment: - if child.text.split()[0] == "": + if child.text.startswith(""): meta_comment = child if self.metadata: child.text = " " + encode(self.metadata) @@ -240,7 +282,9 @@ def save_metadata(self): self.remove(child) if meta_comment is None and self.metadata: - self.append(NodeComment(" " + encode(self.metadata))) + meta_comment = NodeComment() + meta_comment.properties[""].set_value(" " + encode(self.metadata)) + self.add_child(meta_comment) class NodeStandardItem(QStandardItem): @@ -258,7 +302,7 @@ def __lt__(self, other): return False -class NodeInfoRoot(_NodeBase): +class NodeInfoRoot(_NodeElement): """ A node for the tag fomod """ @@ -283,7 +327,7 @@ def _init(self): super()._init() -class NodeInfoName(_NodeBase): +class NodeInfoName(_NodeElement): """ A node for the tag Name """ @@ -302,7 +346,7 @@ def _init(self): super()._init() -class NodeInfoAuthor(_NodeBase): +class NodeInfoAuthor(_NodeElement): """ A node for the tag Author """ @@ -321,7 +365,7 @@ def _init(self): super()._init() -class NodeInfoVersion(_NodeBase): +class NodeInfoVersion(_NodeElement): """ A node for the tag Version """ @@ -340,7 +384,7 @@ def _init(self): super()._init() -class NodeInfoID(_NodeBase): +class NodeInfoID(_NodeElement): """ A node for the tag Id """ @@ -359,7 +403,7 @@ def _init(self): super()._init() -class NodeInfoWebsite(_NodeBase): +class NodeInfoWebsite(_NodeElement): """ A node for the tag Website """ @@ -378,7 +422,7 @@ def _init(self): super()._init() -class NodeInfoDescription(_NodeBase): +class NodeInfoDescription(_NodeElement): """ A node for the tag Description """ @@ -397,7 +441,7 @@ def _init(self): super()._init() -class NodeInfoGroup(_NodeBase): +class NodeInfoGroup(_NodeElement): """ A node for the tag Groups """ @@ -416,7 +460,7 @@ def _init(self): super()._init() -class NodeInfoElement(_NodeBase): +class NodeInfoElement(_NodeElement): """ A node for the tag element """ @@ -435,7 +479,7 @@ def _init(self): super()._init() -class NodeConfigRoot(_NodeBase): +class NodeConfigRoot(_NodeElement): """ A node for the tag config """ @@ -479,7 +523,7 @@ def _init(self): super()._init() -class NodeConfigModName(_NodeBase): +class NodeConfigModName(_NodeElement): """ A node for the tag moduleName """ @@ -501,7 +545,7 @@ def _init(self): super()._init() -class NodeConfigModImage(_NodeBase): +class NodeConfigModImage(_NodeElement): """ A node for the tag moduleImage """ @@ -524,7 +568,7 @@ def _init(self): super()._init() -class NodeConfigModDepend(_NodeBase): +class NodeConfigModDepend(_NodeElement): """ A node for the tag moduleDependencies """ @@ -552,7 +596,7 @@ def _init(self): super()._init() -class NodeConfigReqFiles(_NodeBase): +class NodeConfigReqFiles(_NodeElement): """ A node for the tag requiredInstallFiles """ @@ -574,7 +618,7 @@ def _init(self): super()._init() -class NodeConfigInstallSteps(_NodeBase): +class NodeConfigInstallSteps(_NodeElement): """ A node for the tag installSteps """ @@ -602,7 +646,7 @@ def _init(self): super()._init() -class NodeConfigCondInstall(_NodeBase): +class NodeConfigCondInstall(_NodeElement): """ A node for the tag conditionalFileInstalls """ @@ -626,7 +670,7 @@ def _init(self): super()._init() -class NodeConfigDependFile(_NodeBase): +class NodeConfigDependFile(_NodeElement): """ A node for the tag fileDependency """ @@ -646,7 +690,7 @@ def _init(self): super()._init() -class NodeConfigDependFlag(_NodeBase): +class NodeConfigDependFlag(_NodeElement): """ A node for the tag flagDependency """ @@ -666,7 +710,7 @@ def _init(self): super()._init() -class NodeConfigDependGame(_NodeBase): +class NodeConfigDependGame(_NodeElement): """ A node for the tag gameDependency """ @@ -685,7 +729,7 @@ def _init(self): super()._init() -class NodeConfigFile(_NodeBase): +class NodeConfigFile(_NodeElement): """ A node for the tag file """ @@ -721,7 +765,7 @@ def update_item_name(self): return split_name[len(split_name) - 1] -class NodeConfigFolder(_NodeBase): +class NodeConfigFolder(_NodeElement): """ A node for the tag folder """ @@ -757,7 +801,7 @@ def update_item_name(self): return split_name[len(split_name) - 1] -class NodeConfigPatterns(_NodeBase): +class NodeConfigPatterns(_NodeElement): """ A node for the tag patterns """ @@ -780,7 +824,7 @@ def _init(self): super()._init() -class NodeConfigPattern(_NodeBase): +class NodeConfigPattern(_NodeElement): """ A node for the tag pattern """ @@ -806,7 +850,7 @@ def _init(self): super()._init() -class NodeConfigFiles(_NodeBase): +class NodeConfigFiles(_NodeElement): """ A node for the tag files """ @@ -828,7 +872,7 @@ def _init(self): super()._init() -class NodeConfigDependencies(_NodeBase): +class NodeConfigDependencies(_NodeElement): """ A node for the tag dependencies """ @@ -856,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). """ @@ -883,7 +927,7 @@ def _init(self): super()._init() -class NodeConfigInstallStep(_NodeBase): +class NodeConfigInstallStep(_NodeElement): """ A node for the tag installStep """ @@ -923,7 +967,7 @@ def update_item_name(self): return self.properties["name"].value -class NodeConfigVisible(_NodeBase): +class NodeConfigVisible(_NodeElement): """ A node for the tag visible """ @@ -951,7 +995,7 @@ def _init(self): super()._init() -class NodeConfigOptGroups(_NodeBase): +class NodeConfigOptGroups(_NodeElement): """ A node for the tag optionalFileGroups """ @@ -979,7 +1023,7 @@ def _init(self): super()._init() -class NodeConfigGroup(_NodeBase): +class NodeConfigGroup(_NodeElement): """ A node for the tag group """ @@ -1025,7 +1069,7 @@ def update_item_name(self): return self.properties["name"].value -class NodeConfigPlugins(_NodeBase): +class NodeConfigPlugins(_NodeElement): """ A node for the tag plugins """ @@ -1052,7 +1096,7 @@ def _init(self): super()._init() -class NodeConfigPlugin(_NodeBase): +class NodeConfigPlugin(_NodeElement): """ A node for the tag plugin """ @@ -1096,7 +1140,7 @@ def update_item_name(self): return self.properties["name"].value -class NodeConfigPluginDescription(_NodeBase): +class NodeConfigPluginDescription(_NodeElement): """ A node for the tag description """ @@ -1116,7 +1160,7 @@ def _init(self): super()._init() -class NodeConfigImage(_NodeBase): +class NodeConfigImage(_NodeElement): """ A node for the tag image """ @@ -1136,7 +1180,7 @@ def _init(self): super()._init() -class NodeConfigConditionFlags(_NodeBase): +class NodeConfigConditionFlags(_NodeElement): """ A node for the tag conditionFlags """ @@ -1160,7 +1204,7 @@ def _init(self): super()._init() -class NodeConfigTypeDesc(_NodeBase): +class NodeConfigTypeDesc(_NodeElement): """ A node for the tag typeDescriptor """ @@ -1192,7 +1236,7 @@ def can_add_child(self, child): return False -class NodeConfigFlag(_NodeBase): +class NodeConfigFlag(_NodeElement): """ A node for the tag flag """ @@ -1224,7 +1268,7 @@ def update_item_name(self): return self.properties["name"].value -class NodeConfigDependencyType(_NodeBase): +class NodeConfigDependencyType(_NodeElement): """ A node for the tag dependencyType """ @@ -1249,7 +1293,7 @@ def _init(self): super()._init() -class NodeConfigDefaultType(_NodeBase): +class NodeConfigDefaultType(_NodeElement): """ A node for the tag defaultType """ @@ -1281,7 +1325,7 @@ def update_item_name(self): return self.properties["name"].value -class NodeConfigType(_NodeBase): +class NodeConfigType(_NodeElement): """ A node for the tag type """ @@ -1313,7 +1357,7 @@ def update_item_name(self): return self.properties["name"].value -class NodeConfigInstallPatterns(_NodeBase): +class NodeConfigInstallPatterns(_NodeElement): """ A node for the tag patterns """ @@ -1337,7 +1381,7 @@ def _init(self): super()._init() -class NodeConfigInstallPattern(_NodeBase): +class NodeConfigInstallPattern(_NodeElement): """ A node for the tag pattern """ diff --git a/designer/previews.py b/designer/previews.py index 6fa1257..e671df3 100644 --- a/designer/previews.py +++ b/designer/previews.py @@ -17,7 +17,7 @@ from os.path import join, sep, normpath from queue import Queue from PyQt5.QtCore import QThread -from lxml.etree import XML, tostring +from lxml.etree import XML, tostring, Comment from lxml.objectify import deannotate from pygments import highlight from pygments.formatters.html import HtmlFormatter @@ -77,7 +77,7 @@ def run(self): # wait for next element element = self.queue.get() - if element is None: + if element is None or element.tag is Comment: self.return_signal.emit("") continue diff --git a/tasks.py b/tasks.py index 315caff..05fc63a 100644 --- a/tasks.py +++ b/tasks.py @@ -82,7 +82,7 @@ def build(): from fnmatch import fnmatch # set which files will be included within the archive. - included_files = ["LICENSE", "README.md", "CHANGELOG.rst", "CONTRIBUTING.rst"] + included_files = ["LICENSE"] archive_name = "designer" # the archive's name try: diff --git a/tests/test_io_nodes_props.py b/tests/test_io_nodes_props.py index 6485512..b0065b5 100644 --- a/tests/test_io_nodes_props.py +++ b/tests/test_io_nodes_props.py @@ -18,7 +18,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from designer.io import import_, export, module_parser, new, copy_node, node_factory from designer.exceptions import TagNotFound, ParserError, BaseInstanceException -from designer.nodes import _NodeBase +from designer.nodes import _NodeElement from designer.props import _PropertyBase @@ -55,7 +55,7 @@ def test_exceptions(): import_(os.path.join(os.path.dirname(__file__), "data", "invalid_fomod")) with pytest.raises(BaseInstanceException): - _NodeBase() + _NodeElement() with pytest.raises(BaseInstanceException): _PropertyBase("test", []) From 45c36010dc209c2e801cfbfa2e7c53ead5357220 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Wed, 10 Aug 2016 05:33:25 +0000 Subject: [PATCH 29/31] Closes #46. Added 32bit builds. Code directory is now named src. --- appveyor.yml | 12 ++++++++++-- dev/appveyor-bootstrap.bat | 17 +++++++++++++---- dev/pyinstaller-bootstrap.py | 2 +- dev/travis-test.sh | 2 +- setup.cfg | 2 +- {designer => src}/__init__.py | 0 {designer => src}/__main__.py | 0 {designer => src}/exceptions.py | 0 {designer => src}/gui.py | 0 {designer => src}/io.py | 0 {designer => src}/nodes.py | 0 {designer => src}/previews.py | 0 {designer => src}/props.py | 0 {designer => src}/ui_templates/__init__.py | 0 {designer => src}/ui_templates/preview_mo.py | 0 .../ui_templates/tutorial_advanced.py | 0 {designer => src}/ui_templates/window_about.py | 0 {designer => src}/ui_templates/window_intro.py | 0 .../ui_templates/window_mainframe.py | 0 .../ui_templates/window_plaintexteditor.py | 0 .../ui_templates/window_settings.py | 0 .../ui_templates/window_texteditor.py | 0 .../ui_templates/wizard_depend_01.py | 0 .../ui_templates/wizard_depend_depend.py | 0 .../ui_templates/wizard_depend_depend_depend.py | 0 .../ui_templates/wizard_depend_depend_file.py | 0 .../ui_templates/wizard_depend_depend_flag.py | 0 .../wizard_depend_depend_version.py | 0 .../ui_templates/wizard_depend_file.py | 0 .../ui_templates/wizard_depend_flag.py | 0 .../ui_templates/wizard_files_01.py | 0 .../ui_templates/wizard_files_item.py | 0 {designer => src}/wizards.py | 0 tasks.py | 2 +- tests/test_gui.py | 16 ++++++++-------- tests/test_io_nodes_props.py | 8 ++++---- 36 files changed, 39 insertions(+), 22 deletions(-) rename {designer => src}/__init__.py (100%) rename {designer => src}/__main__.py (100%) rename {designer => src}/exceptions.py (100%) rename {designer => src}/gui.py (100%) rename {designer => src}/io.py (100%) rename {designer => src}/nodes.py (100%) rename {designer => src}/previews.py (100%) rename {designer => src}/props.py (100%) rename {designer => src}/ui_templates/__init__.py (100%) rename {designer => src}/ui_templates/preview_mo.py (100%) rename {designer => src}/ui_templates/tutorial_advanced.py (100%) rename {designer => src}/ui_templates/window_about.py (100%) rename {designer => src}/ui_templates/window_intro.py (100%) rename {designer => src}/ui_templates/window_mainframe.py (100%) rename {designer => src}/ui_templates/window_plaintexteditor.py (100%) rename {designer => src}/ui_templates/window_settings.py (100%) rename {designer => src}/ui_templates/window_texteditor.py (100%) rename {designer => src}/ui_templates/wizard_depend_01.py (100%) rename {designer => src}/ui_templates/wizard_depend_depend.py (100%) rename {designer => src}/ui_templates/wizard_depend_depend_depend.py (100%) rename {designer => src}/ui_templates/wizard_depend_depend_file.py (100%) rename {designer => src}/ui_templates/wizard_depend_depend_flag.py (100%) rename {designer => src}/ui_templates/wizard_depend_depend_version.py (100%) rename {designer => src}/ui_templates/wizard_depend_file.py (100%) rename {designer => src}/ui_templates/wizard_depend_flag.py (100%) rename {designer => src}/ui_templates/wizard_files_01.py (100%) rename {designer => src}/ui_templates/wizard_files_item.py (100%) rename {designer => src}/wizards.py (100%) diff --git a/appveyor.yml b/appveyor.yml index effbd43..ea6456d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,13 +9,21 @@ install: build: off test_script: - - py.test --cov=designer --cov-report html --cov-report term -vv tests/ + - 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: diff --git a/dev/appveyor-bootstrap.bat b/dev/appveyor-bootstrap.bat index 20d668d..58b58e7 100644 --- a/dev/appveyor-bootstrap.bat +++ b/dev/appveyor-bootstrap.bat @@ -1,11 +1,20 @@ @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 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/travis-test.sh b/dev/travis-test.sh index c7a26c3..05f1f0e 100644 --- a/dev/travis-test.sh +++ b/dev/travis-test.sh @@ -21,6 +21,6 @@ eval "$(pyenv virtualenv-init -)" pyenv shell miniconda3-3.19.0/envs/fomod-designer -py.test --cov=designer --cov-report html --cov-report term -vv tests/ +py.test --cov=src --cov-report html --cov-report term -vv tests/ coveralls diff --git a/setup.cfg b/setup.cfg index c9f4998..eb77b84 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,5 +8,5 @@ current_build = 0 universal = 0 [coverage:run] -omit = designer/ui_templates/* +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 100% rename from designer/gui.py rename to src/gui.py diff --git a/designer/io.py b/src/io.py similarity index 100% rename from designer/io.py rename to src/io.py diff --git a/designer/nodes.py b/src/nodes.py similarity index 100% rename from designer/nodes.py rename to src/nodes.py diff --git a/designer/previews.py b/src/previews.py similarity index 100% rename from designer/previews.py rename to src/previews.py 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 100% rename from designer/ui_templates/window_intro.py rename to src/ui_templates/window_intro.py diff --git a/designer/ui_templates/window_mainframe.py b/src/ui_templates/window_mainframe.py similarity index 100% rename from designer/ui_templates/window_mainframe.py rename to src/ui_templates/window_mainframe.py 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 100% rename from designer/wizards.py rename to src/wizards.py diff --git a/tasks.py b/tasks.py index 05fc63a..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)): diff --git a/tests/test_gui.py b/tests/test_gui.py index 5775787..8640416 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -26,8 +26,8 @@ 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 designer import __version__ -from designer.gui import About, read_settings, default_settings, SettingsDialog, generic_errorbox, IntroWindow, \ +from src import __version__ +from src.gui import About, read_settings, default_settings, SettingsDialog, generic_errorbox, IntroWindow, \ MainFrame @@ -47,8 +47,8 @@ def test_about_dialog(qtbot): assert not about_window.isVisible() -@patch('designer.gui.open_new_tab') -@patch('designer.gui.head') +@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 @@ -81,7 +81,7 @@ def test_errorbox(qtbot): assert not errorbox.isVisible() -@patch('designer.gui.open') +@patch('src.gui.open') def test_read_settings(mock_open): mock_open.return_value = StringIO(encode(default_settings)) assert read_settings() == default_settings @@ -102,8 +102,8 @@ def test_read_settings(mock_open): assert read_settings() == default_settings -@patch('designer.gui.read_settings') -@patch('designer.gui.open') +@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)) @@ -170,7 +170,7 @@ def test_settings_dialog(mock_open, mock_read_settings, qtbot, tmpdir): assert decode(settings_file.read()) == settings_window.settings_dict -@patch("designer.gui.read_settings") +@patch("src.gui.read_settings") def test_intro(mock_read_settings, qtbot): settings_dict = default_settings settings_dict["General"]["tutorial_advanced"] = False diff --git a/tests/test_io_nodes_props.py b/tests/test_io_nodes_props.py index b0065b5..43d5d88 100644 --- a/tests/test_io_nodes_props.py +++ b/tests/test_io_nodes_props.py @@ -16,10 +16,10 @@ import sys, os, lxml, pytest sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from designer.io import import_, export, module_parser, new, copy_node, node_factory -from designer.exceptions import TagNotFound, ParserError, BaseInstanceException -from designer.nodes import _NodeElement -from designer.props import _PropertyBase +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): From dcc96d0f7dd0b1c5545daa5a4ae1af5f9e7ac8b2 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 11 Aug 2016 02:31:22 +0000 Subject: [PATCH 30/31] Improved contributing document. --- CONTRIBUTING.rst | 62 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index c5910eb..a34774d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -20,27 +20,59 @@ info required in the pull request template. Contributing Code +++++++++++++++++ -This repo uses a ``.settings`` file to define all the necessary settings. This file follows this syntax: +**General Guidelines**: -.. code-block:: ini - - [git] - user=git_username - email=git_email + * 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. -Fork the repo. + * Follow the `style guide `_. -Make your change and make sure the tests pass on the CI. Follow the `style guide `_. + * Write `decent commit messages `_. -.. todo:: - Integrate tox with vagrant/conda/pyenv + * 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. -Push to your fork. Write a `good commit message `_. Submit a pull request. +**Setup the work environment**: -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. + 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 `_! From a408314a5291aa5de82544d25e29053dc2122c74 Mon Sep 17 00:00:00 2001 From: Daniel Nunes Date: Thu, 11 Aug 2016 02:53:40 +0000 Subject: [PATCH 31/31] Version bump and updated changelog. --- CHANGELOG.rst | 9 +++++++++ docs/source/conf.py | 17 +++++++++++++---- setup.cfg | 4 +--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b17550..935e663 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +**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. diff --git a/docs/source/conf.py b/docs/source/conf.py index 3a4e55b..52eb5ed 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,6 +19,7 @@ # import os import sys +from configparser import ConfigParser sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))) # -- General configuration ------------------------------------------------ @@ -63,10 +64,18 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = '0.4.1' -# The full version, including alpha/beta/rc tags. -release = '0.4.1' +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. diff --git a/setup.cfg b/setup.cfg index eb77b84..3d4acfb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,7 @@ [bumpversion] -current_version = 0.7.2 +current_version = 0.8.0 current_build = 0 -[bumpversion:file:docs/source/conf.py] - [bdist_wheel] universal = 0