diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..663f8f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +*.build +*.dist +*.exe \ No newline at end of file diff --git a/COPYING.txt b/COPYING.txt new file mode 100644 index 0000000..75b452c --- /dev/null +++ b/COPYING.txt @@ -0,0 +1,1151 @@ +Treemendous is available under the Mozilla Public License Version 2.0. Treemendous also includes and uses components which are made available under different free and open source licences. + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +Python +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +Additional Conditions for this Windows binary build +--------------------------------------------------- + +This program is linked with and uses Microsoft Distributable Code, +copyrighted by Microsoft Corporation. The Microsoft Distributable Code +is embedded in each .exe, .dll and .pyd file as a result of running +the code through a linker. + +If you further distribute programs that include the Microsoft +Distributable Code, you must comply with the restrictions on +distribution specified by Microsoft. In particular, you must require +distributors and external end users to agree to terms that protect the +Microsoft Distributable Code at least as much as Microsoft's own +requirements for the Distributable Code. See Microsoft's documentation +(included in its developer tools and on its website at microsoft.com) +for specific details. + +Redistribution of the Windows binary build of the Python interpreter +complies with this agreement, provided that you do not: + +- alter any copyright, trademark or patent notice in Microsoft's +Distributable Code; + +- use Microsoft's trademarks in your programs' names or in a way that +suggests your programs come from or are endorsed by Microsoft; + +- distribute Microsoft's Distributable Code to run on a platform other +than Microsoft operating systems, run-time technologies or application +platforms; or + +- include Microsoft Distributable Code in malicious, deceptive or +unlawful programs. + +These restrictions apply only to the Microsoft Distributable Code as +defined above, not to Python itself or any programs running on the +Python interpreter. The redistribution of the Python interpreter and +libraries is governed by the Python Software License included with this +file, or by other licenses as marked. + + + +-------------------------------------------------------------------------- + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2010 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, jseward@bzip.org +bzip2/libbzip2 version 1.0.6 of 6 September 2010 + +-------------------------------------------------------------------------- + + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + + +libffi - Copyright (c) 1996-2014 Anthony Green, Red Hat, Inc and others. +See source files for details. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +``Software''), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This software is copyrighted by the Regents of the University of +California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState +Corporation and other parties. The following terms apply to all files +associated with the software unless explicitly disclaimed in +individual files. + +The authors hereby grant permission to use, copy, modify, distribute, +and license this software and its documentation for any purpose, provided +that existing copyright notices are retained in all copies and that this +notice is included verbatim in any distributions. No written agreement, +license, or royalty fee is required for any of the authorized uses. +Modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided that +the new terms are clearly indicated on the first page of each file where +they apply. + +IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE +IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE +NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. + +GOVERNMENT USE: If you are acquiring this software on behalf of the +U.S. government, the Government shall have only "Restricted Rights" +in the software and related documentation as defined in the Federal +Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +are acquiring the software on behalf of the Department of Defense, the +software shall be classified as "Commercial Computer Software" and the +Government shall have only "Restricted Rights" as defined in Clause +252.227-7014 (b) (3) of DFARs. Notwithstanding the foregoing, the +authors grant the U.S. Government and others acting in its behalf +permission to use and distribute the software in accordance with the +terms specified in this license. + +This software is copyrighted by the Regents of the University of +California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState +Corporation, Apple Inc. and other parties. The following terms apply to +all files associated with the software unless explicitly disclaimed in +individual files. + +The authors hereby grant permission to use, copy, modify, distribute, +and license this software and its documentation for any purpose, provided +that existing copyright notices are retained in all copies and that this +notice is included verbatim in any distributions. No written agreement, +license, or royalty fee is required for any of the authorized uses. +Modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided that +the new terms are clearly indicated on the first page of each file where +they apply. + +IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE +IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE +NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. + +GOVERNMENT USE: If you are acquiring this software on behalf of the +U.S. government, the Government shall have only "Restricted Rights" +in the software and related documentation as defined in the Federal +Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +are acquiring the software on behalf of the Department of Defense, the +software shall be classified as "Commercial Computer Software" and the +Government shall have only "Restricted Rights" as defined in Clause +252.227-7013 (b) (3) of DFARs. Notwithstanding the foregoing, the +authors grant the U.S. Government and others acting in its behalf +permission to use and distribute the software in accordance with the +terms specified in this license. + +Copyright (c) 1993-1999 Ioi Kim Lam. +Copyright (c) 2000-2001 Tix Project Group. +Copyright (c) 2004 ActiveState + +This software is copyrighted by the above entities +and other parties. The following terms apply to all files associated +with the software unless explicitly disclaimed in individual files. + +The authors hereby grant permission to use, copy, modify, distribute, +and license this software and its documentation for any purpose, provided +that existing copyright notices are retained in all copies and that this +notice is included verbatim in any distributions. No written agreement, +license, or royalty fee is required for any of the authorized uses. +Modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided that +the new terms are clearly indicated on the first page of each file where +they apply. + +IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY +FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY +DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE +IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE +NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. + +GOVERNMENT USE: If you are acquiring this software on behalf of the +U.S. government, the Government shall have only "Restricted Rights" +in the software and related documentation as defined in the Federal +Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you +are acquiring the software on behalf of the Department of Defense, the +software shall be classified as "Commercial Computer Software" and the +Government shall have only "Restricted Rights" as defined in Clause +252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the +authors grant the U.S. Government and others acting in its behalf +permission to use and distribute the software in accordance with the +terms specified in this license. + +---------------------------------------------------------------------- + +Parts of this software are based on the Tcl/Tk software copyrighted by +the Regents of the University of California, Sun Microsystems, Inc., +and other parties. The original license terms of the Tcl/Tk software +distribution is included in the file docs/license.tcltk. + +Parts of this software are based on the HTML Library software +copyrighted by Sun Microsystems, Inc. The original license terms of +the HTML Library software distribution is included in the file +docs/license.html_lib. + + +Graphviz +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. +1. DEFINITIONS +"Contribution" means: +“ +a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and +b) in the case of each subsequent Contributor: +i) changes to the Program, and +ii) additions to the Program; +where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. +"Contributor" means any person or entity that distributes the Program. +"Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. +"Program" means the Contributions distributed in accordance with this Agreement. +"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. +2. GRANT OF RIGHTS +“ +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. +3. REQUIREMENTS +A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: +“ +a) it complies with the terms and conditions of this Agreement; and +b) its license agreement: +i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; +ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; +iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and +iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. +When the Program is made available in source code form: +“ +a) it must be made available under this Agreement; and +b) a copy of this Agreement must be included with each copy of the Program. +Contributors may not remove or alter any copyright notices contained within the Program. +Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. +4. COMMERCIAL DISTRIBUTION +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. +5. NO WARRANTY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. +6. DISCLAIMER OF LIABILITY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. +7. GENERAL +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. +If Recipient institutes patent litigation against a Contributor with respect to a patent applicable to software (including a cross-claim or counterclaim in a lawsuit), then any patent licenses granted by that Contributor to such Recipient under this Agreement shall terminate as of the date such litigation is filed. In addition, if Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. IBM is the initial Agreement Steward. IBM may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. +This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. + +Python Graphviz package +The MIT License (MIT) + +Copyright (c) 2013-2021 Sebastian Bank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + wxWindows Library Licence, Version 3.1 + ====================================== + + Copyright (c) 1998-2005 Julian Smart, Robert Roebling et al + + Everyone is permitted to copy and distribute verbatim copies + of this licence document, but changing it is not allowed. + + WXWINDOWS LIBRARY LICENCE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + This library is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public Licence as published by + the Free Software Foundation; either version 2 of the Licence, or (at + your option) any later version. + + This library is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library + General Public Licence for more details. + + You should have received a copy of the GNU Library General Public Licence + along with this software, usually in a file named COPYING.LIB. If not, + write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301 USA. + + EXCEPTION NOTICE + + 1. As a special exception, the copyright holders of this library give + permission for additional uses of the text contained in this release of + the library as licenced under the wxWindows Library Licence, applying + either version 3.1 of the Licence, or (at your option) any later version of + the Licence as published by the copyright holders of version + 3.1 of the Licence document. + + 2. The exception is that you may use, copy, link, modify and distribute + under your own terms, binary object code versions of works based + on the Library. + + 3. If you copy code from files distributed under the terms of the GNU + General Public Licence or the GNU Library General Public Licence into a + copy of this library, as this licence permits, the exception does not + apply to the code that you add in this way. To avoid misleading anyone as + to the status of such modified files, you must delete this exception + notice from such code and/or adjust the licensing conditions notice + accordingly. + + 4. If you write modifications of your own for this library, it is your + choice whether to permit this exception to apply to your modifications. + If you do not wish that, you must delete the exception notice from such + code and/or adjust the licensing conditions notice accordingly. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cbe6831 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Treemendous +Treemendous is an accessible tree creation and exploration tool especially designed for blind and vision impaired practitioners and students of linguistics and computer science. + +![A screenshot of the Treemendous interface showing a syntax tree of the sentence "I touch the cactus" alongside a graphical rendering of the tree.](https://user-images.githubusercontent.com/2476107/144321528-087596c4-c583-466d-a2fc-1fd924d921e9.png) + +## Getting Treemendous +The [latest version of Treemendous](https://github.com/codeofdusk/treemendous/releases/latest) and sample trees in Treemendous format are available on GitHub. Windows is the recommended operating system for which binary installer and portable packages are provided. Some support for non-Windows systems may be available, but it is incomplete and requires running Treemendous from source. + +## Using Treemendous +### Opening a tree +On launch, Treemendous opens to an empty tree. A Treemendous file can be opened by selecting "open" from the file menu, pressing the standard Ctrl+o keyboard shortcut, or, if file associations were selected at install time, directly from Windows Explorer. + +#### Sample trees +[Examples of completed tree diagrams in Treemendous format](https://github.com/codeofdusk/treemendous/releases/latest) taken from linguistics and computer science are available on GitHub. To view or edit one of the sample files, simply open it as described above. + +### Exploring the tree +When Treemendous opens, focus is set to a tree control containing the tree's contents. To explore the tree, use the arrow keys, mouse, or, if on MacOS, the VoiceOver cursor (including VO+backslash to expand/collapse a node). If using a screen reader, based on screen reader configuration, the currently focused level, location, and expanded/collapsed state will be reported as you navigate. + +### Adding a node +To add a new node, press the "add" button in the window or use the keyboard shortcut Alt+a. If the tree is currently empty, a dialog box for adding a root node will be displayed. Otherwise, a menu of node locations will appear containing the following options: + +* Child: The newly added node will be contained by the current selection. +* Parent: The newly added node will contain the current selection. +* Sibling: The newly added node will be placed at the same level as the current selection. + +Once a node location has been selected, the "add node" dialog will appear, containing fields for label and value. In computer science, the label and value of a node might represent the key and value, respectively, of a key–value pair represented by the newly added node. In a syntax tree, the label might contain the name of a syntactic category, such as N, and the represented word would be entered into the value field. If this node does not represent such a pair though, simply enter the node text into the label field and leave the value blank. When finished, press OK to add the new node to the tree. + +### Editing a node +To edit the currently selected node, press F2 or select "edit node" from the Shift+F10/right click context menu. An edit node dialog will appear, from which the node's label and value can be changed. Press OK to save changes or cancel to close the dialog without saving. + +### HTML-like formatting +The following HTML-like tags can be placed anywhere in a node's label or value to produce formatted text. For ease of editing, raw tags are shown in the Treemendous interface. However, well-formed formatted text will appear when the tree is exported to an alternative format. + +Tag | Description | Example +--- | --- | --- +b | Bold | `This text is bold` +i | Italic | `This text is in italics` +u | Underline | `The word underlined is underlined` +sup | Superscript | `Di has a superscript i` +sub | Subscript | `x1 has a subscript 1` +null | Empty set symbol | `` +bar | Superscript prime symbol, used in [X-bar theory](https://en.wikipedia.org/wiki/X-bar_theory) | `X` + +### Moving a node +To adjust the position of a node within its parent, use the move up/move down options in the node's Shift+F10/right click context menu, or Alt+up/down arrows. + +The selected subtree (node and all descendants) can be copied and pasted both within the current Treemendous tree and across other opened files, as long as all trees are opened in the same Treemendous process (second, third, etc. instances created by selecting "new" from the file menu). To mark the current selection for copying, press Ctrl+c or select "copy" from the edit menu on the menu bar. Then, at the point where you wish to paste, press Ctrl+v or select "paste" from the edit menu. + +### Deleting nodes +To delete the currently selected subtree (node and all descendants), select delete from the Shift+F10/right click menu, or press Del on the keyboard. + +### Notes +It may be helpful to include notes in a Treemendous file, such as the sentence from which a syntactic tree was generated or source attributions. To show or hide the notes field, select "notes" from the view menu on the menu bar. When shown, the notes field can be focused with the tab key or the keyboard shortcut Alt+n. + +### Saving +To save the currently opened tree, select "save" or "save as" from the file menu on the menu bar, or use the standard Ctrl+s keyboard shortcut for save or Ctrl+Shift+s for save as. Trees can be saved in the following formats, either by selecting from the "save as type" combo box or naming the file with the associated file extension: + +Format | File extension | Description +--- | --- | --- +Treemendous | .treemendous | Used for viewing and editing the tree in an accessible format using Treemendous. +Graphviz | .gv | A plain text representation of the tree for use with [Graphviz](https://graphviz.org/). +PNG | .png | The [portable network graphics](https://en.wikipedia.org/wiki/Portable_Network_Graphics) image format. + +### Visual representation +To view a graphical representation of the opened Treemendous tree, for instance to aid collaboration with sighted colleagues/instructors, select "visual" from the view menu on the menu bar. Press Esc to close the visual view and return to Treemendous. + +### LaTeX representation +To view a plain text representation of the currently opened Treemendous tree for inclusion in a [LaTeX](https://www.latex-project.org/) document, select LaTeX from the view menu on the menu bar. Note that the [Qtree](https://ctan.org/pkg/qtree) package is required and must be included in the document preamble. Press Esc to close the LaTeX view and return to Treemendous. + +## Developing Treemendous +Note: The following assumes that `python` and `pip` refer to Python version 3.6 or later. On some systems, you may need to run `python3` or `pip3` instead. + +### Running from source +From the root of the repo, install dependancies with `pip install -Ur requirements.txt`, then run `python src/treemendous.py` to start the GUI. + +### Running unit tests +To run unit tests for the `tree` module, run `python src/test_tree.py`. diff --git a/channels/stable.json b/channels/stable.json new file mode 100644 index 0000000..0494408 --- /dev/null +++ b/channels/stable.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "latest_version": "1.0.0", + "minimum_version": "1.0.0", + "release_page": "https://github.com/codeofdusk/treemendous/releases/tag/v1.0.0" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c9415aa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +graphviz +setuptools +wxpython diff --git a/samples/BST.treemendous b/samples/BST.treemendous new file mode 100644 index 0000000..212b05e Binary files /dev/null and b/samples/BST.treemendous differ diff --git a/samples/maxheap.treemendous b/samples/maxheap.treemendous new file mode 100644 index 0000000..e9f3775 Binary files /dev/null and b/samples/maxheap.treemendous differ diff --git a/samples/syntax.treemendous b/samples/syntax.treemendous new file mode 100644 index 0000000..821fda6 Binary files /dev/null and b/samples/syntax.treemendous differ diff --git a/src/menus.py b/src/menus.py new file mode 100644 index 0000000..cbcb6d0 --- /dev/null +++ b/src/menus.py @@ -0,0 +1,180 @@ +""" +This module contains various context menus used in the main GUI. +Copyright 2021 Bill Dengler and open-source contributors +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +""" + +import gettext +import wx + +_ = gettext.translation("treemendous", fallback=True).gettext + + +class AddNodeMenu(wx.Menu): + def __init__(self, parent): + super().__init__() + self.parent = parent + + child = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: The option for adding a child in the add node pop-up menu. + _("&Child"), + # Translators: Help text in the add node pop-up menu. + _("Add a new node as an immediate child of the current selection"), + ) + self.Append(child) + self.Bind(wx.EVT_MENU, self.OnChild, child) + + parent = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: The option for adding a parent in the add node pop-up menu. + _("&Parent"), + # Translators: Help text in the add node pop-up menu. + _("Add a new node that contains the currently selected subtree"), + ) + self.Append(parent) + self.Bind(wx.EVT_MENU, self.OnParent, parent) + + if self.parent.tree.selection != self.parent.tree.root: + sibling = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: The option for adding a sibling in the add node pop-up menu. + _("&Sibling"), + _( + # Translators: Help text in the add node pop-up menu. + "Add a new node as an immediate sibling (same level) of the current selection" + ), + ) + self.Append(sibling) + self.Bind(wx.EVT_MENU, self.OnSibling, sibling) + + def OnChild(self, event): + return self.parent.DoAddChild() + + def OnParent(self, event): + return self.parent.DoAddParent() + + def OnSibling(self, event): + return self.parent.DoAddSibling() + + +class PasteDestMenu(wx.Menu): + def __init__(self, parent, event): + super().__init__() + self.parent = parent + self.event = event + + child = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: An option in the paste pop-up menu. + _("As &child"), + # Translators: Help text in the paste pop-up menu. + _("Paste as an immediate child of the current selection"), + ) + self.Append(child) + self.Bind(wx.EVT_MENU, self.OnChild, child) + + parent = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: An option in the paste pop-up menu. + _("As &parent"), + # Translators: Help text in the paste pop-up menu. + _("Merge the pasteboard with the current selection."), + ) + self.Append(parent) + self.Bind(wx.EVT_MENU, self.OnParent, parent) + + if self.parent.tree.selection != self.parent.tree.root: + sibling = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: An option in the paste pop-up menu. + _("As &sibling"), + _( + # Translators: Help text in the paste pop-up menu. + "Paste as an immediate sibling (same level) of the current selection" + ), + ) + self.Append(sibling) + self.Bind(wx.EVT_MENU, self.OnSibling, sibling) + + def OnChild(self, event): + return self.parent.PasteChild(self.event) + + def OnParent(self, event): + return self.parent.PasteParent(self.event) + + def OnSibling(self, event): + return self.parent.PasteSibling(self.event) + + +class NodeContextMenu(wx.Menu): + def __init__(self, parent): + super().__init__() + self.parent = parent + + addSubmenu = AddNodeMenu(parent) + self.AppendSubMenu( + addSubmenu, + # Translators: An item in the node context (shift+f10) menu. + _("&Add node"), + help=_( + # Translators: Help text in the node context (shift+F10) menu. + "Add a new node relative to the current selection." + ), + ) + + edit = wx.MenuItem( + self, + wx.ID_EDIT, + # Translators: An item in the node context (shift+f10) menu. + _("Edit node...\tF2"), + # Translators: Help text in the node context (shift+F10) menu. + _("Edit the currently selected node."), + ) + self.Append(edit) + + up = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: An item in the node context (shift+f10) menu. + _("Move up\tAlt+up"), + # Translators: Help text in the node context (shift+F10) menu. + _("Move subtree to previous position in parent."), + ) + self.Append(up) + self.Bind(wx.EVT_MENU, self.OnUp, up) + + dn = wx.MenuItem( + self, + wx.ID_ANY, + # Translators: An item in the node context (shift+f10) menu. + _("Move down\tAlt+down"), + # Translators: Help text in the node context (shift+F10) menu. + _("Move subtree to next position in parent."), + ) + self.Append(dn) + self.Bind(wx.EVT_MENU, self.OnDn, dn) + + delsubtree = wx.MenuItem( + self, + wx.ID_DELETE, + # Translators: An item in the node context (shift+f10) menu. + _("&Delete subtree\tDEL"), + # Translators: Help text in the node context (shift+F10) menu. + _("Delete this node and all of its descendants."), + ) + self.Append(delsubtree) + + def OnUp(self, event): + return self.parent.OnMoveUp(event) + + def OnDn(self, event): + return self.parent.OnMoveDown(event) diff --git a/src/test_tree.py b/src/test_tree.py new file mode 100644 index 0000000..4a59810 --- /dev/null +++ b/src/test_tree.py @@ -0,0 +1,179 @@ +""" +Unit tests for tree module. +Copyright 2021 Bill Dengler and open-source contributors +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +""" + +import unittest + +from tree import Node + + +class TestNode(unittest.TestCase): + SIMPLE_TREE_DICT = { + "label": "TP", + "value": None, + "children": [ + {"label": "DP", "value": None, "children": []}, + {"label": "T", "value": None, "children": []}, + ], + } + + SIMPLE_QTREE = "\Tree [.TP\n" " DP\n" " T$^{\prime}$\n" "]\n" + + def test_init_empty(self): + n = Node() + self.assertIsNone(n.label) + self.assertIsNone(n.value) + self.assertEqual(len(n.children), 0) + self.assertIsNone(n.parent) + + def test_string_empty(self): + n = Node() + self.assertEqual(str(n), "UNLABELLED") + + def test_string_label_only(self): + n = Node() + n.label = "TP" + self.assertEqual(str(n), "TP") + + def test_string_label_and_value(self): + n = Node() + n.label = "D" + n.value = "I" + self.assertEqual(str(n), "D: I") + + def test_string_value_only(self): + n = Node() + n.value = "val" + self.assertEqual(str(n), "UNLABELLED: val") + + def test_add_child(self): + tp = Node() + dp = Node() + tp.label = "TP" + dp.label = "DP" + tp.add_child(dp) + self.assertIn(dp, tp.children) + self.assertEqual(dp.parent, tp) + + def test_add_child_connected(self): + tp = Node(label="TP") + dp = Node(label="DP") + tp.add_child(dp) + tp2 = Node(label="TP") + with self.assertRaises(AssertionError): + tp2.add_child(dp) + + def test_add_parent(self): + dp = Node(label="DP") + d = Node(label="D", value="the") + dp.add_child(d) + n = Node(label="N", value="cactus") + dp.add_child(n) + np = Node(label="NP") + n.add_parent(np) + self.assertIn(np, dp.children) + self.assertEqual(np, n.parent) + self.assertNotIn(n, dp.children) + + def test_add_parent_replacing_root(self): + n = Node(label="N", value="cacti") + np = Node(label="NP") + with self.assertRaises(AssertionError): + n.add_parent(np) + + def test_delete(self): + dp = Node(label="DP") + d = Node(label="D", value="the") + dp.add_child(d) + np = Node(label="NP") + dp.add_child(np) + n = Node(label="N", value="cactus") + np.add_child(n) + np.delete() + self.assertNotIn(np, dp.children) + self.assertNotIn(n, dp.children) + + def test_delete_root(self): + n = Node() + with self.assertRaises(AssertionError): + n.delete() + + def test_from_dict_empty(self): + n = Node.from_dict({}) + self.assertIsNone(n.label) + self.assertIsNone(n.value) + self.assertEqual(len(n.children), 0) + self.assertIsNone(n.parent) + + def test_from_dict_label_only(self): + n = Node.from_dict({"label": "TP"}) + self.assertEqual(n.label, "TP") + self.assertIsNone(n.value) + self.assertEqual(len(n.children), 0) + self.assertIsNone(n.parent) + + def test_from_dict_label_and_value(self): + n = Node.from_dict({"label": "D", "value": "I"}) + self.assertEqual(n.label, "D") + self.assertEqual(n.value, "I") + self.assertEqual(len(n.children), 0) + self.assertIsNone(n.parent) + + def test_from_dict_value_only(self): + n = Node.from_dict({"value": "val"}) + self.assertIsNone(n.label) + self.assertEqual(n.value, "val") + self.assertEqual(len(n.children), 0) + self.assertIsNone(n.parent) + + def test_from_dict_simple(self): + tp = Node.from_dict(TestNode.SIMPLE_TREE_DICT) + self.assertEqual(tp.label, "TP") + self.assertIsNone(tp.value) + self.assertEqual(len(tp.children), 2) + self.assertIsNone(tp.parent) + dp = tp.children[0] + self.assertEqual(dp.label, "DP") + self.assertIsNone(dp.value) + self.assertEqual(len(dp.children), 0) + self.assertEqual(dp.parent, tp) + tbar = tp.children[1] + self.assertEqual(tbar.label, "T") + self.assertIsNone(tbar.value) + self.assertEqual(len(tbar.children), 0) + self.assertEqual(tbar.parent, tp) + + def test_to_dict_simple(self): + tp = Node(label="TP") + dp = Node(label="DP") + tbar = Node(label="T") + tp.add_child(dp) + tp.add_child(tbar) + self.assertEqual(tp.to_dict(), TestNode.SIMPLE_TREE_DICT) + + def test_qtree_simple(self): + self.assertEqual( + Node.from_dict(TestNode.SIMPLE_TREE_DICT).to_qtree(), TestNode.SIMPLE_QTREE + ) + + def test_qtree_degenerate(self): + self.assertEqual(Node(label="root").to_qtree(), "\Tree [.root\n]\n") + + def test_qtree_bold(self): + self.assertEqual( + Node(label="root").to_qtree(), "\Tree [.\\textbf{root}\n]\n" + ) + + def test_qtree_bold_unclosed(self): + self.assertEqual(Node(label="root").to_qtree(), "\Tree [.root\n]\n") + + def test_qtree_bold_unopened(self): + self.assertEqual(Node(label="root").to_qtree(), "\Tree [.root\n]\n") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/tree.py b/src/tree.py new file mode 100644 index 0000000..abef588 --- /dev/null +++ b/src/tree.py @@ -0,0 +1,449 @@ +""" +This module contains the Node and Tree classes, for storing and manipulating tree data respectively. +Third-party implementations/interfaces should instantiate Tree and primarily call its methods. Data can be accessed from the root attribute, which contains the tree's root Node. +Copyright 2021 Bill Dengler and open-source contributors +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +""" + +__version__ = "1.0.0rc3" + +import gettext +import html +import io +import json +import os +import zipfile + +from collections import deque +from enum import Enum, auto +from html.parser import HTMLParser +from tempfile import mktemp +from typing import Optional, Set + +_ = gettext.translation("treemendous", fallback=True).gettext + + +class GVParser(HTMLParser): + TEX_MAP = { + "b": "\\textbf{", + "i": "\\textit{", + "u": "\\underline{", + "sup": "^{", + "sub": "_{", + "null": "{\O", + "bar": "^{\prime", + } + MATHMODE_REQUIRED = ("sup", "sub", "null", "bar") + SPECIALS = ("null", "bar") + + def reset(self, *args, **kwargs): + self.valid = True + self._tag_stack = deque() + self._math_stack = deque() + self.tex = "" + self.data = "" + return super().reset(*args, **kwargs) + + def close(self, *args, **kwargs): + if self._tag_stack: # If we have unclosed tags + self.valid = False + return super().close(*args, **kwargs) + + def handle_starttag(self, tag, attrs): + if attrs: + self.valid = False + if tag not in GVParser.TEX_MAP: + self.valid = False + self.tex += f"<{tag}>" + else: + self._tag_stack.append(tag) + if tag in GVParser.MATHMODE_REQUIRED: + if not self._math_stack: + self.tex += "$" + self._math_stack.append(tag) + self.tex += GVParser.TEX_MAP[tag] + if ( + tag in GVParser.SPECIALS + ): # some tags should be part of data (for node IDs, etc) + self.data += tag.capitalize() + + def handle_endtag(self, tag): + try: + self.valid = self._tag_stack.pop() in GVParser.TEX_MAP + except IndexError: + self.valid = False + if tag in GVParser.TEX_MAP: + self.tex += "}" + else: + self.tex += f"" + if tag in GVParser.MATHMODE_REQUIRED: + self._math_stack.pop() + if not self._math_stack: + self.tex += "$" + + def handle_data(self, data): + self.tex += data + self.data += data + + +class Node: + def __init__(self, label: str = None, value: str = None): + self.label = label + self.value = value + self.children = [] + self.parent = None + + @classmethod + def from_dict(cls, data: dict) -> "Node": + res = cls(label=data.get("label"), value=data.get("value")) + for c in data.get("children", []): + res.add_child(cls.from_dict(c)) + return res + + def __repr__(self) -> str: + # Translators: Placeholder text for a node without label. + res = _("UNLABELLED") + if self.label: + res = self.label + if self.value: + res += ": " + self.value + return res + + def to_dict(self) -> dict: + return { + "label": self.label, + "value": self.value, + "children": [c.to_dict() for c in self.children], + } + + def to_qtree(self) -> str: + def _qtree(node: Node, parser: HTMLParser, level: int = 0) -> str: + parser.reset() + parser.feed(node.label) + parser.close() + lbl = parser.tex if parser.valid else node.label + if node.value: + parser.reset() + parser.feed(node.value) + parser.close() + val = parser.tex if parser.valid else node.value + else: + val = None + leaf = not node.children and level > 0 + res = " " * level + f"{'[.' if not leaf else ''}{lbl}" + if val: + res += f"\\\\{val}" + res += "\n" + for c in node.children: + res += _qtree(c, parser, level + 1) + if not leaf: + res += " " * level + "]\n" + return res + + parser = GVParser() + return "\\Tree " + _qtree(self, parser) + + def to_graphviz( + self, + dpi: Optional[int] = None, + graph: Optional["graphviz.Graph"] = None, + name_set: Optional[Set[str]] = None, + ) -> "graphviz.Graph": + def _fresh_name(name: str, names: Set[str]): + if name == "": # Some nodes in angle brackets have a blank ID + name = "node" + res = name + num = 1 + while res in names: + num += 1 + res = f"{name}{num}" + names.add(res) + return res + + def _is_valid(text: str, parser: HTMLParser) -> bool: + parser.reset() + parser.feed(text) + parser.close() + return parser.valid + + def _escape_if_needed(text: str, parser: HTMLParser) -> str: + cleaned_text = text + REPLACEMENTS = {"": "Ø", "": ""} + for src, dest in REPLACEMENTS.items(): + cleaned_text = cleaned_text.replace(src, dest) + valid = _is_valid(text, parser) + if valid: + return cleaned_text + else: + return html.escape(text) + + def _add_node( + node: Node, + graph: "graphviz.Graph", + name_set: Set[str], + parser: HTMLParser, + parent: Node = None, + ) -> None: + parser.reset() + parser.feed(node.label) + id = _fresh_name(parser.data, name_set) + if node.value: + label = f"<{_escape_if_needed(node.label, parser)}
{_escape_if_needed(node.value, parser)}>" + else: + label = _escape_if_needed(node.label, parser) + if _is_valid(label, parser): + label = "<" + label + ">" + graph.node(id, label) + if parent: + graph.edge(parent, id) + for c in node.children: + _add_node(c, graph, name_set, parser, parent=id) + + import graphviz + + graph = graphviz.Graph( + format="png", + node_attr={"shape": "plain"}, + graph_attr={ + "dpi": str(dpi) if dpi is not None else "400", + "nodesep": ".25", + "ranksep": "0.02", + }, + ) # ranksep is height of edges in inches, minimum is 0.02 + name_set = set() + parser = GVParser() + _add_node(self, graph, name_set, parser) + return graph + + def add_child(self, c: "Node") -> None: + assert c.parent is None + self.children.append(c) + c.parent = self + + def delete(self) -> None: + assert self.parent is not None # Deleting the root is a special case + self.parent.children.remove(self) + + def add_parent(self, node: "Node") -> None: + assert self.parent is not None # Replacing the root is a special case + assert node.parent is None + i = self.parent.children.index(self) + node.parent = self.parent + del self.parent.children[i] + self.parent.children.insert(i, node) + self.parent = None + node.add_child(self) + + +class Location(Enum): + CHILD = auto() + PARENT = auto() + SIBLING = auto() + + +class TreemendousError(Exception): + pass + + +class SaveError(TreemendousError): + pass + + +class SelectionError(TreemendousError): + pass + + +class IncompatibleFormatError(TreemendousError): + pass + + +DEFAULT_MANIFEST: dict = {"version": __version__} + + +_pasteboard: dict = None + + +class Tree: + def __init__(self, path: str = None): + self.dirty: bool = False + self.last_path: str = path or "" + self.selection: Node = None + self.manifest: dict = DEFAULT_MANIFEST.copy() + + if path: + try: + with zipfile.ZipFile(path) as zip: + with zip.open("manifest.json") as fin: + m = json.load(fin) + self.manifest.update(m) + my_major = int(__version__.split(".")[0]) + their_major = int(self.manifest["version"].split(".")[0]) + if my_major < their_major: + raise IncompatibleFormatError( + _( + # Translators: A warning message displayed when a file is too new for the current Treemendous version. + "This file is too new for the currently running version of Treemendous ({my_version}). Please upgrade to Treemendous {their_version} or later." + ).format( + my_version=__version__, + their_version=f"{their_major}.0.0", + ) + ) + with zip.open("tree.json") as fin: + d = json.load(fin) + self.root: Node = Node.from_dict(d) + except (KeyError, zipfile.BadZipFile): + raise IncompatibleFormatError( + # Translators: An error message displayed when a Treemendous file could not be read. + _("Invalid, very outdated, or damaged Treemendous file.") + ) + else: + self.root: Node = None + + @property + def is_empty(self) -> bool: + "Returns True on trees that do not contain any nodes." + return not self.root + + @property + def notes(self) -> str: + "Free-form notes that can be entered and displayed along with the tree, such as the phrase from which a syntax tree was constructed." + return self.manifest.get("notes", "") + + @notes.setter + def notes(self, new: str): + self.manifest["notes"] = new + self.dirty = True + + def save(self, path: str = None) -> None: + "Save the tree as a .treemendous, .gv (Graphviz markup), or .png file." + if not path: + if self.last_path: + path = self.last_path + else: + raise SaveError("No last path") + if path.endswith(".gv"): + g = self.root.to_graphviz() + if self.last_path: + g.name = os.path.splitext(os.path.basename(self.last_path))[0] + return g.save(path) + if path.endswith(".png"): + return self.graphviz(path) + with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_LZMA) as zip: + with zip.open("tree.json", "w") as cam: + json.dump(self.root.to_dict(), io.TextIOWrapper(cam), indent=2) + with zip.open("manifest.json", "w") as cam: + json.dump(self.manifest, io.TextIOWrapper(cam), indent=2) + self.dirty = False + self.last_path = path + + def qtree(self) -> str: + "Renders this tree as LaTeX (dependant on qtree) markup." + # Translators: The comment added at the top of a LaTeX document. The \usepackage{qtree} is LaTeX code that should not be translated. + COMMENT = _("Add \\usepackage{qtree} to the preamble of your document.") + return f"% {COMMENT}\n\n{self.root.to_qtree()}" + + def graphviz(self, path: str = None, dpi: Optional[int] = None) -> None: + "Renders this tree as a .png image using Graphviz." + g = self.root.to_graphviz(dpi=dpi) + if path is None: + path = mktemp(prefix="treemendous") + else: + path = os.path.splitext(path)[0] + if self.last_path: + g.name = os.path.splitext(os.path.basename(self.last_path))[0] + return g.render(path, cleanup=True) + + def _add(self, where: Location, new: Node) -> None: + assert isinstance(where, Location) + if self.is_empty: + self.root = new + elif self.selection is None: + raise SelectionError("No selection!") + elif where == Location.CHILD: + self.selection.add_child(new) + elif where == Location.PARENT: + if self.selection == self.root: + new.add_child(self.root) + self.root = new + else: + self.selection.add_parent(new) + elif where == Location.SIBLING: + if self.selection == self.root: + raise TreemendousError("The root cannot have siblings!") + self.selection.parent.add_child(new) + self.selection = new + self.dirty = True + + def add(self, where: Location, label: str = None, value: str = None) -> None: + "Adds a new node at the location specified as a member of the Location enumeration in this module." + if label == "": + label = None + if value == "": + value = None + new = Node(label, value) + return self._add(where=where, new=new) + + def edit(self, label: str = None, value: str = None) -> None: + "Edits the currently selected node." + if label is not None: + if label == "": + self.selection.label = None + self.dirty = True + else: + self.selection.label = label + self.dirty = True + if value is not None: + if value == "": + self.selection.value = None + self.dirty = True + else: + self.selection.value = value + self.dirty = True + + def delete(self) -> None: + "Deletes the currently selected node and all descendants." + if not self.selection: + raise SelectionError("No selection!") + n = self.selection + if n == self.root: + self.root = None + else: + self.selection = n.parent + n.delete() + self.dirty = True + + def copy(self) -> None: + "Copys the current selection to a (Treemendous internal) pasteboard." + if self.selection is None: + raise SelectionError("No selection!") + global _pasteboard + _pasteboard = self.selection.to_dict() + + def paste(self, where: Location) -> None: + "Pastes the contents of the (Treemendous internal) pasteboard to the location specified as a member of the Location enumeration in this module." + global _pasteboard + if not _pasteboard: + raise SelectionError("Pasteboard is empty!") + new = Node.from_dict(_pasteboard) + self._add(where=where, new=new) + + def _shift(self, direction: int) -> None: + if not self.selection: + raise SelectionError("No selection!") + elif self.selection == self.root: + raise TreemendousError("Cannot shift the root!") + t = self.selection.parent.children + old = t.index(self.selection) + new = old + direction + t.insert(new, t.pop(old)) + self.dirty = True + + def move_up(self) -> None: + "Moves the currently selected node up relative to its siblings." + return self._shift(-1) + + def move_down(self) -> None: + "Moves the currently selected node down relative to its siblings." + return self._shift(1) diff --git a/src/treectrl.py b/src/treectrl.py new file mode 100644 index 0000000..f345355 --- /dev/null +++ b/src/treectrl.py @@ -0,0 +1,158 @@ +""" +Tree control class, containing an abstract interface to Windows, MacOS, and GTK native tree widgets. +Copyright 2021 Bill Dengler and open-source contributors +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +""" + +import wx +import wx.dataview + +from abc import ABC, abstractmethod +from enum import auto, Enum + + +class TreeEvent(Enum): + ITEM_SELECTED = auto() + ITEM_EXPANDED = auto() + ITEM_COLLAPSED = auto() + CONTEXT_MENU = auto() + KEY_DOWN = auto() + + +class TreeCTRL(ABC): + "Base tree control interface." + + @property + @abstractmethod + def widget(self): + raise NotImplementedError + + @abstractmethod + def AddRoot(self, node, expanded): + raise NotImplementedError + + @abstractmethod + def AddChild(self, node, expanded): + raise NotImplementedError + + @abstractmethod + def BindEvent(self, evt, handler): + raise NotImplementedError + + @abstractmethod + def DeleteAll(self): + raise NotImplementedError + + @abstractmethod + def CollapseChildren(self, itm): + raise NotImplementedError + + @abstractmethod + def GetNodeFromItem(self, itm): + raise NotImplementedError + + @abstractmethod + def Select(self, itm): + raise NotImplementedError + + +class WinTreeCTRL(TreeCTRL): + def __init__(self, *args, **kwargs): + self._inner = wx.TreeCtrl(*args, **kwargs) + + @property + def widget(self): + return self._inner + + def AddRoot(self, node, expanded): + itm = self._inner.AddRoot(str(node)) + self._inner.SetItemData(itm, node) + if expanded: + self._inner.Expand(itm) + return itm + + def AddChild(self, rootitm, node, expanded): + itm = self._inner.AppendItem(rootitm, str(node)) + self._inner.SetItemData(itm, node) + if expanded: + self._inner.Expand(itm) + return itm + + def BindEvent(self, evt, handler): + TreeEventsToWXEvents = { + TreeEvent.ITEM_SELECTED: wx.EVT_TREE_SEL_CHANGED, + TreeEvent.ITEM_EXPANDED: wx.EVT_TREE_ITEM_EXPANDED, + TreeEvent.ITEM_COLLAPSED: wx.EVT_TREE_ITEM_COLLAPSED, + TreeEvent.CONTEXT_MENU: wx.EVT_TREE_ITEM_MENU, + TreeEvent.KEY_DOWN: wx.EVT_KEY_DOWN, + } + return self._inner.Bind(TreeEventsToWXEvents[evt], handler) + + def DeleteAll(self): + return self._inner.DeleteAllItems() + + def CollapseChildren(self, itm): + return self._inner.CollapseAllChildren(itm) + + def GetNodeFromItem(self, itm): + return self._inner.GetItemData(itm) + + def Select(self, itm): + return self._inner.SelectItem(itm) + + +class MacTreeCTRL(TreeCTRL): + def __init__(self, *args, **kwargs): + self._inner = wx.dataview.DataViewTreeCtrl(*args, **kwargs) + self._inner.Bind( + wx.dataview.EVT_DATAVIEW_ITEM_START_EDITING, lambda event: event.Veto() + ) # block editing + + @property + def widget(self): + return self._inner + + def AddRoot(self, node, expanded): + itm = self._inner.AppendContainer( + wx.dataview.NullDataViewItem, str(node), expanded=expanded, data=node + ) + return itm + + def AddChild(self, rootitm, node, expanded): + if node.children: + itm = self._inner.AppendContainer( + rootitm, str(node), expanded=expanded, data=node + ) + else: + itm = self._inner.AppendItem(rootitm, str(node), data=node) + return itm + + def BindEvent(self, evt, handler): + TreeEventsToWXEvents = { + TreeEvent.ITEM_SELECTED: wx.dataview.EVT_DATAVIEW_SELECTION_CHANGED, + TreeEvent.ITEM_EXPANDED: wx.dataview.EVT_DATAVIEW_ITEM_EXPANDED, + TreeEvent.ITEM_COLLAPSED: wx.dataview.EVT_DATAVIEW_ITEM_COLLAPSED, + TreeEvent.CONTEXT_MENU: wx.dataview.EVT_DATAVIEW_ITEM_CONTEXT_MENU, + TreeEvent.KEY_DOWN: wx.EVT_KEY_DOWN, + } + if evt == TreeEvent.CONTEXT_MENU: + # VoiceOver seems not to be able to activate the context menu (VO+shift+m does nothing). + # As a fallback, also bind to item activation. + self._inner.Bind(wx.dataview.EVT_DATAVIEW_ITEM_ACTIVATED, handler) + return self._inner.Bind(TreeEventsToWXEvents[evt], handler) + + def DeleteAll(self): + return self._inner.DeleteAllItems() + + def CollapseChildren(self, itm): + return self._inner.Collapse( + itm + ) # DataViewTreeCtrl seems not to support collapsing children + + def GetNodeFromItem(self, itm): + return self._inner.GetItemData(itm) + + def Select(self, itm): + return self._inner.Select(itm) diff --git a/src/treemendous.py b/src/treemendous.py new file mode 100644 index 0000000..74c229f --- /dev/null +++ b/src/treemendous.py @@ -0,0 +1,935 @@ +""" +This module contains the Treemendous GUI and is the entry point for the program. +Copyright 2021 Bill Dengler and open-source contributors +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +""" + +# Build-time options +# nuitka-project: --standalone +# nuitka-project: --enable-plugin=anti-bloat +# nuitka-project-if: {OS} == "Windows": +# nuitka-project: --windows-disable-console +# nuitka-project-if: {OS} == "Darwin": +# nuitka-project: --macos-disable-console +# nuitka-project: --macos-create-app-bundle + + +import argparse +import gettext +import json +import os +import platform +import urllib.request +import webbrowser +import wx + +from graphviz import ExecutableNotFound as GraphvizNotFound +from json.decoder import JSONDecodeError +from menus import AddNodeMenu, NodeContextMenu, PasteDestMenu +from pkg_resources import packaging +from sys import maxsize +from tree import ( + __version__, + IncompatibleFormatError, + Location, + SaveError, + SelectionError, + Tree, +) +from treectrl import MacTreeCTRL, TreeEvent, WinTreeCTRL +from urllib.error import URLError + +_ = gettext.translation("treemendous", fallback=True).gettext + +# Translators: The name of the Treemendous file format, to be shown in the "files of type" combobox. +TREEMENDOUS_FMT = _("Treemendous files") +# Translators: The name of the Graphviz file format, to be shown in the "files of type" combobox. +GRAPHVIZ_FMT = _("Graphviz files") +# Translators: a label to be shown in the "files of type" combobox. +PNG_FMT = _("Images") + +WILDCARD = f"{TREEMENDOUS_FMT} (*.treemendous)|*.treemendous" +# TODO: According to the WXPython docs, the native Motif dialog can't handle +# this. If someone shouts, try and detect Motif and use WILDCARD instead of +# SAVE_WILDCARD (disabling gv functionality) or show split dialogs for +# GV/Treemendous saving. +SAVE_WILDCARD = ( + f"{TREEMENDOUS_FMT} (*.treemendous)|*.treemendous" + f"|{GRAPHVIZ_FMT} (*.gv)|*.gv" + f"|{PNG_FMT} (*.png)|*.png" +) + +GRAPHVIZ_DOWNLOAD_URL = "https://graphviz.org/download/" + +AUTOUPDATE_ENDPOINT = "https://raw.githubusercontent.com/codeofdusk/treemendous/master/channels/stable.json" +AUTOUPDATE_SCHEMA_VERSION = 1 + + +class EditNodeDialog(wx.Dialog): + def __init__(self, title, label=None, value=None): + if label is None: + label = "" + if value is None: + value = "" + super().__init__(parent=None, title=title) + panel = wx.Panel(self) + vbox = wx.BoxSizer(wx.VERTICAL) + fgs = wx.FlexGridSizer(2, 2, 5, 5) + # label box + labelLbl = wx.StaticText( + panel, + # Translators: The label for the node label field in the edit node dialog. + label=_("&Label:"), + ) + self.label = wx.TextCtrl(panel, value=label) + # value box + valueLbl = wx.StaticText( + panel, + # Translators: The label for the node value field in the edit node dialog. + label=_("&Value:"), + ) + self.value = wx.TextCtrl(panel, value=value) + # put boxes into sizer + fgs.AddMany( + [ + (labelLbl), + (self.label, 1, wx.EXPAND), + (valueLbl), + (self.value, 1, wx.EXPAND), + ] + ) + fgs.AddGrowableCol(1, 1) + vbox.Add( + fgs, proportion=1, flag=wx.TOP | wx.RIGHT | wx.LEFT | wx.EXPAND, border=5 + ) + # button box + btnSizer = wx.BoxSizer(wx.HORIZONTAL) + ok = wx.Button(panel, wx.ID_OK) + ok.SetDefault() + cancel = wx.Button(panel, wx.ID_CANCEL) + btnSizer.Add(ok) + btnSizer.Add(cancel) + vbox.Add(btnSizer, flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=5) + # bring everything together + panel.SetSizer(vbox) + + +class ReadOnlyViewDialog(wx.Dialog): + def __init__(self, title, text): + super().__init__(parent=None, title=title) + mainSizer = wx.BoxSizer(wx.VERTICAL) + self.output = wx.TextCtrl( + self, + wx.ID_ANY, + size=(500, 500), + style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2, + value=text, + ) + self.output.Bind(wx.EVT_KEY_DOWN, self.onKeyDown) + mainSizer.Add(self.output, proportion=1, flag=wx.EXPAND) + self.SetSizer(mainSizer) + mainSizer.Fit(self) + + def onKeyDown(self, event): + key = event.GetKeyCode() + if key == wx.WXK_ESCAPE: + return self.Close() + event.Skip() + + +class VisualViewDialog(wx.Dialog): + def __init__(self, path, platform): + self.path = path + self.platform = platform + file = wx.Image(path, wx.BITMAP_TYPE_ANY).ConvertToBitmap() + (self.imgWidth, self.imgHeight) = (file.GetWidth(), file.GetHeight()) + self.imgSize = wx.Size(self.imgWidth, self.imgHeight) + self.imgProportion = self.imgWidth / self.imgHeight + + super().__init__( + parent=None, + # Translators: The title of a dialog used to show the visual representation of a tree. + title=_("Visual rendering"), + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, + size=self.imgSize, + ) + self.Bind(wx.EVT_SIZE, self.OnSize) + self.mainSizer = wx.BoxSizer(wx.VERTICAL) + self.bmp = wx.Bitmap(self.path) + self.img = wx.StaticBitmap( + self, wx.ID_ANY, self.bmp, (0, 0), (self.imgWidth, self.imgHeight) + ) + self.Bind( + wx.EVT_CHAR_HOOK, self.onCharHook + ) ## Use EVT_CHAR_HOOK here, because dialogs don't send EVT_KEY_DOWN on all platforms + self.mainSizer.Add(self.img, proportion=1, flag=wx.EXPAND | wx.ALL, border=15) + self.SetSizerAndFit(self.mainSizer) + + def onCharHook(self, event): + key = event.GetKeyCode() + if key == wx.WXK_ESCAPE: + return self.Close() + event.Skip() + + def OnSize(self, event): + (dialogWidth, dialogHeight) = self.GetSize() + if self.platform not in ("Windows", "Darwin"): + # magic numbers are needed to make sure the image doesn't go outside the bounds of the dialog window + dialogWidthAdjust = -50 + dialogHeightAdjust = -90 + dialogWidth += dialogWidthAdjust + dialogHeight += dialogHeightAdjust + dialogProportion = dialogWidth / dialogHeight + if dialogProportion > self.imgProportion: + newHeight = dialogHeight + newWidth = self.imgWidth * (dialogHeight / self.imgHeight) + else: + newHeight = self.imgHeight * (dialogWidth / self.imgWidth) + newWidth = dialogWidth + # make sure we're scaling from a fresh load of the image + self.img.SetBitmap(self.scaleBitmap(self.bmp, newWidth, newHeight)) + self.Refresh() + + def scaleBitmap(self, bitmap, width, height): + image = bitmap.ConvertToImage() + image = image.Scale(round(width), round(height), wx.IMAGE_QUALITY_HIGH) + result = wx.Bitmap(image) + return result + + +class Editor(wx.Frame): + def __init__(self, path=None, system=None): + wx.Frame.__init__(self, parent=None, title="Treemendous") + + # Variables. + self.tree = Tree() + self.treectrl = None + self._expanded = [] + if system: + self.platform = system + else: + self.platform = platform.system() + if self.platform != "Windows": + msg = wx.MessageDialog( + self, + _( + # Translators: A message shown when a user launches Treemendous on an unsupported OS. + "Support for your operating system ({platform}) has not been fully verified by the Treemendous developers. If you proceed, a baseline level of functionality and accessibility may be present, but the user experience may not be complete or native to your system. In particular, tree nodes may appear in the wrong order, and some keyboard commands may not function at all or as intended. If the usual commands for opening the context menu on a tree node do not work, try pressing enter or double-clicking the selected node with the mouse. If the arrow keys do not expand/collapse nodes, try plus/minus." + ).format(platform=self.platform), + # Translators: The title of a message box. + _("OS not supported"), + wx.OK | wx.OK_DEFAULT | wx.CANCEL | wx.ICON_WARNING, + ) + if msg.ShowModal() != wx.ID_OK: + raise RuntimeError("OS not supported") + + self.AutoUpdate() + + # Setting up menubar. + menubar = wx.MenuBar() + + file = wx.Menu() + new = wx.MenuItem( + file, + wx.ID_NEW, + # Translators: An item in the file menu. + _("&New\tCtrl+n"), + # Translators: Help text for "new" in the file menu. + _("Creates a new blank Treemendous instance."), + ) + file.Append(new) + + open = wx.MenuItem( + file, + wx.ID_OPEN, + # Translators: An item in the file menu. + _("&Open\tCtrl+O"), + # Translators: Help text for "open" in the file menu. + _("Open an existing tree."), + ) + file.Append(open) + file.AppendSeparator() + + save = wx.MenuItem( + file, + wx.ID_SAVE, + # Translators: An item in the file menu. + _("&Save\tCtrl+S"), + # Translators: Help text for "save" in the file menu. + _("Save this tree to disk."), + ) + file.Append(save) + + saveas = wx.MenuItem( + file, + wx.ID_SAVEAS, + # Translators: An item in the file menu. + _("Save &as...\tShift+Ctrl+S"), + # Translators: Help text for "save as" in the file menu. + _("Save this tree to a different location."), + ) + file.Append(saveas) + file.AppendSeparator() + + quit = wx.MenuItem( + file, + wx.ID_EXIT, + # Translators: An item in the file menu. + _("&Quit\tCtrl+Q"), + # Translators: Help text for "quit" in the file menu. + _("Exit Treemendous."), + ) + file.Append(quit) + + edit = wx.Menu() + copy = wx.MenuItem( + edit, + wx.ID_COPY, + # Translators: An item in the edit menu. + _("Copy\tCTRL+c"), + # Translators: Help text for "copy" in the edit menu. + "Copy the currently selected node to the pasteboard.", + ) + paste = wx.MenuItem( + edit, + wx.ID_PASTE, + # Translators: An item in the edit menu. + _("Paste\tCTRL+v"), + _( + # Translators: Help text for "paste" in the edit menu. + "Place the contents of the pasteboard in the tree at the specified position." + ), + ) + edit.Append(copy) + edit.Append(paste) + + view = wx.Menu() + self.viewVisualMenuItem = wx.MenuItem( + view, + wx.ID_ANY, + # Translators: An item in the "view" menu. + _("&Visual"), + _( + # Translators: Help text for "visual" in the view menu. + "Show a graphical representation of this tree." + ), + ) + view.Append(self.viewVisualMenuItem) + self.NotesCheckBox = wx.MenuItem( + view, + wx.ID_ANY, + # Translators: An item in the "view" menu that toggles the display of the notes window. + # The notes window allows users to enter freeform text along with their tree. + _("&Notes"), + _( + # Translators: Help text for "notes" in the view menu. + "Show or hide the notes window, which allows entry of freeform text to be shown along with the tree." + ), + kind=wx.ITEM_CHECK, + ) + view.Append(self.NotesCheckBox) + self.qtreeMenuItem = wx.MenuItem( + view, + wx.ID_ANY, + # Translators: An item in the "view" menu. + _("La&TeX (Qtree)"), + _( + # Translators: Help text for "LaTeX" in the view menu. + "Show this tree as source code suitable for pasting into a LaTeX document. Requires that the qtree package be included in the document preamble." + ), + ) + view.Append(self.qtreeMenuItem) + + help = wx.Menu() + about = wx.MenuItem( + help, + wx.ID_ABOUT, + # Translators: An item in the help menu. + _("&About\tF1"), + # Translators: Help text for the "about" option in the help menu. Please indicate that this dialog is always in English. + _("View version and licence."), + ) + help.Append(about) + + menubar.Append( + file, + # Translators: The name of a menu in the menu bar. + _("&File"), + ) + menubar.Append( + edit, + # Translators: The name of a menu in the menu bar. + _("&Edit"), + ) + menubar.Append( + view, + # Translators: The name of a menu in the menu bar. + _("&View"), + ) + menubar.Append( + help, + # Translators: The name of a menu in the menu bar. + _("&Help"), + ) + + self.SetMenuBar(menubar) + + self.panel = wx.Panel(self) + + notesSizer = wx.BoxSizer(wx.VERTICAL) + self.notesLbl = wx.StaticText( + self.panel, + # Translators: The label for the notes window. + label=_("&Notes:"), + ) + notesSizer.Add(self.notesLbl) + self.notesField = wx.TextCtrl( + self.panel, style=wx.TE_MULTILINE | wx.TE_RICH2, value=self.tree.notes + ) + notesSizer.Add(self.notesField, flag=wx.EXPAND | wx.TOP | wx.BOTTOM) + + self.notesField.Bind(wx.EVT_TEXT, self.OnNotesChanged) + + self.addNodeButton = wx.Button( + self.panel, + # Translators: The label of the add node button in the main window. + label=_("&Add..."), + ) + self.addNodeButton.Bind(wx.EVT_BUTTON, self.OnAddNode) + + self.Bind(wx.EVT_MENU, self.NewInstance, id=wx.ID_NEW) + self.Bind(wx.EVT_MENU, self.OnOpenFile, id=wx.ID_OPEN) + self.Bind(wx.EVT_MENU, self.OnSaveFile, id=wx.ID_SAVE) + self.Bind(wx.EVT_MENU, self.OnSaveAsFile, id=wx.ID_SAVEAS) + self.Bind(wx.EVT_MENU, self.QuitApplication, id=wx.ID_EXIT) + self.Bind(wx.EVT_MENU, self.OnCopy, id=wx.ID_COPY) + self.Bind(wx.EVT_MENU, self.OnPaste, id=wx.ID_PASTE) + self.Bind(wx.EVT_MENU, self.OnEditNode, id=wx.ID_EDIT) + self.Bind(wx.EVT_MENU, self.OnDeleteNode, id=wx.ID_DELETE) + self.Bind(wx.EVT_MENU, self.OnViewVisual, self.viewVisualMenuItem) + self.Bind(wx.EVT_MENU, self.OnToggleNotes, self.NotesCheckBox) + self.Bind(wx.EVT_MENU, self.OnQtree, self.qtreeMenuItem) + self.Bind(wx.EVT_MENU, self.OnAbout, id=wx.ID_ABOUT) + self.Bind(wx.EVT_CLOSE, self.QuitApplication) + + fgs = wx.FlexGridSizer(3, 1, 5, 5) + self.InitTree() + fgs.Add(self.treectrl.widget, wx.ID_ANY, wx.EXPAND | wx.ALL, border=3) + fgs.Add( + notesSizer, + wx.ID_ANY, + flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, + border=3, + ) + fgs.Add(self.addNodeButton, flag=wx.LEFT | wx.RIGHT, border=3) + + fgs.AddGrowableRow(0, 1) + fgs.AddGrowableCol(0, 1) + + self.panel.SetSizer(fgs) + + self.StatusBar() + + self.Centre() + + if path is not None: + self.OpenTree(path) + + self.RenderTree() + + notesenabled = bool(self.tree.notes) + self.EnableNotes(notesenabled) + + self.Show() + + def InitTree(self): + ctrl = WinTreeCTRL if self.platform == "Windows" else MacTreeCTRL + self.treectrl = ctrl(self.panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize) + self.treectrl.BindEvent(TreeEvent.ITEM_SELECTED, self.OnSelectionChanged) + self.treectrl.BindEvent(TreeEvent.ITEM_EXPANDED, self.OnExpand) + self.treectrl.BindEvent(TreeEvent.ITEM_COLLAPSED, self.OnCollapse) + self.treectrl.BindEvent(TreeEvent.CONTEXT_MENU, self.OnNodeContextMenu) + self.treectrl.BindEvent(TreeEvent.KEY_DOWN, self.OnTreeKeyDown) + + def RenderTree(self): + sel = self.tree.selection + root = None + toExpand = [] + + def _initializeLevel(guiRoot, treeRoot): + r = self.treectrl.AddChild(guiRoot, treeRoot, treeRoot in self._expanded) + if treeRoot == sel: + self.treectrl.Select(r) + for c in treeRoot.children: + _initializeLevel(r, c) + + self.UpdateName() + + self.viewVisualMenuItem.Enable(not self.tree.is_empty) + self.qtreeMenuItem.Enable(not self.tree.is_empty) + + if not self.treectrl: + self.InitTree() + else: + self.treectrl.DeleteAll() + if not self.tree.is_empty: + root = self.treectrl.AddRoot( + self.tree.root, self.tree.root in self._expanded + ) + if sel is None or sel == self.tree.root: + self.treectrl.Select(root) + for c in self.tree.root.children: + _initializeLevel(root, c) + self.treectrl.widget.SetFocus() + + def UpdateName(self): + title = "Treemendous" + if self.tree.last_path: + name = os.path.splitext(os.path.basename(self.tree.last_path))[0] + title = f"{name} – {title}" + if self.tree.dirty: + title = f"*{title}" + self.SetTitle(title) + + def AutoUpdate(self): + try: + req = urllib.request.Request( + AUTOUPDATE_ENDPOINT, + headers={ + "User-Agent": f"Mozilla/5.0 (compatible; python-Treemendous/{__version__}; +https://github.com/codeofdusk/treemendous)" + }, + ) + resp = json.loads(urllib.request.urlopen(req, timeout=10).read()) + except (URLError, JSONDecodeError) as e: + # Translators: Part of a message printed to the command line when the update check could not be completed. + UPDATE_FAIL = _("Error while checking for updates:") + print(f"{UPDATE_FAIL} {e}") + return + if AUTOUPDATE_SCHEMA_VERSION < resp.get("schema_version", maxsize): + return self.UpdateAvailable(required=True) + my_version = packaging.version.parse(__version__) + latest_version = packaging.version.parse(resp["latest_version"]) + minimum_version = packaging.version.parse(resp["minimum_version"]) + if my_version < minimum_version: + return self.UpdateAvailable( + version=resp["latest_version"], page=resp["release_page"], required=True + ) + elif my_version < latest_version: + return self.UpdateAvailable( + version=resp["latest_version"], + page=resp["release_page"], + required=False, + ) + + def NewInstance(self, event): + editor = Editor(system=self.platform) + editor.Centre() + editor.Show() + + def OnOpenFile(self, event): + file_name = os.path.basename(self.tree.last_path) + + if self.tree.dirty: + dlg = wx.MessageDialog( + self, + # Translators: The text of a prompt asking if the user wants to save unsaved changes (for instance, when closing the program or opening a new tree over top of the current). + _("Save changes ?"), + "Treemendous", + wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION, + ) + + val = dlg.ShowModal() + if val == wx.ID_YES: + self.OnSaveFile(event) + self.DoOpenFile() + elif val == wx.ID_CANCEL: + dlg.Destroy() + else: + self.DoOpenFile() + else: + self.DoOpenFile() + + def OpenTree(self, path): + try: + self.tree = Tree(path) + self._expanded = [] + self.EnableNotes(bool(self.tree.notes)) + except (IncompatibleFormatError, IOError) as e: + dlg = wx.MessageDialog( + self, + str(e), + # Translators: The title of an error dialog shown when a Treemendous file could not be opened. + _("Error"), + wx.ICON_ERROR, + ) + dlg.ShowModal() + + def DoOpenFile(self): + open_dlg = wx.FileDialog( + self, + # Translators: The title of the open file dialog. + message=_("Choose tree"), + defaultDir=os.getcwd(), + defaultFile="", + wildcard=WILDCARD, + style=wx.FD_OPEN | wx.FD_CHANGE_DIR | wx.FD_FILE_MUST_EXIST | wx.FD_PREVIEW, + ) + + if open_dlg.ShowModal() == wx.ID_OK: + path = open_dlg.GetPath() + self.OpenTree(path) + self.RenderTree() + self.statusbar.SetStatusText("", 1) + open_dlg.Destroy() + + def OnSaveFile(self, event): + try: + self.tree.save() + self.statusbar.SetStatusText("", 1) + self.UpdateName() + except IOError as error: + dlg = wx.MessageDialog(self, "Error saving file\n" + str(error)) + dlg.ShowModal() + except SaveError: + self.OnSaveAsFile(event) + + def OnSaveAsFile(self, event): + save_dlg = wx.FileDialog( + self, + # Translators: The title of the "save tree as" dialog. + message=_("Save tree as ..."), + defaultDir=os.getcwd(), + defaultFile="", + wildcard=SAVE_WILDCARD, + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + ) + save_dlg.SetFilterIndex(0) + + if save_dlg.ShowModal() == wx.ID_OK: + path = save_dlg.GetPath() + + if path.endswith(".png"): + msg = wx.MessageDialog( + self, + _( + # Translators: A message shown when a user tries to save an image. + "Image files are inaccessible to screen reader users. To ensure accessibility, please provide an additional accessible format, such as a textual discription, Treemendous file, or Graphviz source code whereever you distribute this image." + ), + # Translators: The title of a message box. + _("Accessibility warning"), + wx.OK | wx.OK_DEFAULT | wx.CANCEL | wx.ICON_WARNING, + ) + if msg.ShowModal() != wx.ID_OK: + return + + try: + self.tree.save(path=path) + self.statusbar.SetStatusText(self.tree.last_path + " Saved", 0) + self.statusbar.SetStatusText("", 1) + self.UpdateName() + except GraphvizNotFound: + self.GetGraphviz() + except IOError as error: + # Translators: Text displayed in a message box before the OS error message when a file could not be saved. + HEADER = _("Error saving file") + dlg = wx.MessageDialog(self, f"{HEADER}\n{error}") + dlg.ShowModal() + save_dlg.Destroy() + + def QuitApplication(self, event): + if self.tree.dirty: + dlg = wx.MessageDialog( + self, + # Translators: The text of a prompt asking if the user wants to save unsaved changes (for instance, when closing the program or opening a new tree over top of the current). + _("Save changes ?"), + "Treemendous", + wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION, + ) + val = dlg.ShowModal() + if val == wx.ID_YES: + self.OnSaveFile(event) + if not self.tree.dirty: + wx.Exit() + elif val == wx.ID_CANCEL: + dlg.Destroy() + else: + self.Destroy() + else: + self.Destroy() + + def OnSelectionChanged(self, event): + self.tree.selection = self.treectrl.GetNodeFromItem(event.GetItem()) + + def OnExpand(self, event): + itm = event.GetItem() + if itm.IsOk: + self._expanded.append(self.treectrl.GetNodeFromItem(itm)) + + def OnCollapse(self, event): + itm = event.GetItem() + if itm.IsOk: + self._expanded.remove(self.treectrl.GetNodeFromItem(itm)) + self.treectrl.CollapseChildren(itm) + + def OnNotesChanged(self, event): + self.tree.notes = self.notesField.GetValue() + self.UpdateName() + + def OnToggleNotes(self, event): + notesenabled = not self.notesField.Shown + self.EnableNotes(notesenabled) + + def EnableNotes(self, notesenabled): + # Changing the value of the notes field causes self.notes to be updated and the dirty flag to be set. + # Unbind the event handler temporarily when refreshing the field to avoid this. + self.notesField.Unbind(wx.EVT_TEXT) + self.notesField.SetValue(self.tree.notes) + self.notesField.Bind(wx.EVT_TEXT, self.OnNotesChanged) + self.notesLbl.Show(notesenabled) + self.notesField.Show(notesenabled) + self.NotesCheckBox.Check(notesenabled) + self.panel.GetSizer().Layout() # Update control sizing after show/hide + + def OnAddNode(self, event): + if not self.tree.is_empty: + self.PopupMenu(AddNodeMenu(self)) + else: + self.DoAddChild() + + def DoAddChild(self): + title = ( + # Translators: The name of the dialog for adding the first node to a tree. + _("Add root") + if self.tree.is_empty + # Translators: The name of the dialog for adding a child (contained item) to a node labelled {label}. + else _("Add child of {label}").format(label=self.tree.selection.label) + ) + dlg = EditNodeDialog(title=title) + if dlg.ShowModal() == wx.ID_OK: + self.tree.add(Location.CHILD, dlg.label.GetValue(), dlg.value.GetValue()) + self.RenderTree() + dlg.Destroy() + + def DoAddParent(self): + # Translators: The name of the dialog for adding a parent (containg item) to a node labelled {label}. + title = _("Add parent of {label}").format(label=self.tree.selection.label) + dlg = EditNodeDialog(title=title) + if dlg.ShowModal() == wx.ID_OK: + self.tree.add(Location.PARENT, dlg.label.GetValue(), dlg.value.GetValue()) + self.RenderTree() + dlg.Destroy() + + def DoAddSibling(self): + # Translators: The name of the dialog for adding a sibling (item on same level) to a node labelled {label}. + title = _("Add sibling of {label}").format(label=self.tree.selection.label) + dlg = EditNodeDialog(title=title) + if dlg.ShowModal() == wx.ID_OK: + self.tree.add(Location.SIBLING, dlg.label.GetValue(), dlg.value.GetValue()) + self.RenderTree() + dlg.Destroy() + + def OnCopy(self, event): + self.tree.copy() + + def OnPaste(self, event): + if not self.tree.is_empty: + self.PopupMenu(PasteDestMenu(self, event)) + else: + self.DoPaste(Location.CHILD, event) + + def DoPaste(self, location, event): + try: + self.tree.paste(location) + self.RenderTree() + except SelectionError: # Raised when the pasteboard is empty + event.Skip() + + def PasteChild(self, event): + self.DoPaste(Location.CHILD, event) + + def PasteParent(self, event): + self.DoPaste(Location.PARENT, event) + + def PasteSibling(self, event): + self.DoPaste(Location.SIBLING, event) + + def OnNodeContextMenu(self, event): + if self.tree.is_empty: + return event.Skip() + self.PopupMenu(NodeContextMenu(self)) + + def OnTreeKeyDown(self, event): + keycode = event.GetKeyCode() + if event.AltDown(): + if keycode == wx.WXK_UP: + return self.OnMoveUp(event) + elif keycode == wx.WXK_DOWN: + return self.OnMoveDown(event) + if keycode == wx.WXK_F2: + return self.OnEditNode(event) + elif keycode == wx.WXK_DELETE: + return self.OnDeleteNode(event) + else: + return event.Skip() + + def OnEditNode(self, event): + dlg = EditNodeDialog( + # Translators: The name of the dialog for editing a node labelled {label}. + title=_("Editing {label}").format(label=self.tree.selection.label), + label=self.tree.selection.label, + value=self.tree.selection.value, + ) + if dlg.ShowModal() == wx.ID_OK: + self.tree.edit(label=dlg.label.GetValue(), value=dlg.value.GetValue()) + self.RenderTree() + dlg.Destroy() + + def OnDeleteNode(self, event): + # Translators: A confirmation message asking if the user wants to delete this node. Options are OK and cancel. + LEAF_MSG = _("Are you sure that you want to delete this node?") + NONLEAF_MSG = _( + # Translators: A confirmation message asking if the user wants to delete this node and all of its descendants (children, grandchildren, etc.). Options are OK and cancel. + "Are you sure that you want to delete this node and all descendants?" + ) + dlg = wx.MessageDialog( + self, + LEAF_MSG if not self.tree.selection.children else NONLEAF_MSG, + # Translators: Title of a message dialog confirming deletion of the node labelled {label}. + _("Delete {label}").format(label=self.tree.selection.label), + wx.OK | wx.OK_DEFAULT | wx.CANCEL | wx.ICON_WARNING, + ) + + val = dlg.ShowModal() + if val == wx.ID_OK: + self.tree.delete() + self.RenderTree() + + def OnMoveUp(self, event): + self.tree.move_up() + self.RenderTree() + + def OnMoveDown(self, event): + self.tree.move_down() + self.RenderTree() + + def StatusBar(self): + self.statusbar = self.CreateStatusBar() + self.statusbar.SetFieldsCount(3) + self.statusbar.SetStatusWidths([-5, -2, -1]) + + def GetGraphviz(self): + dlg = wx.MessageDialog( + self, + _( + # Translators: Text of a message shown when the user tries to use a function that requires Graphviz but doesn't have it installed. + "Treemendous requires that Graphviz is installed to perform this action, but it could not be found. If you proceed, the Graphviz website will be opened in your web browser so that you can download and install it. During installation, if prompted, please select to have Graphviz added to the system path. You may need to restart Treemendous after installation." + ), + # Translators: The title of a message box. + _("Graphviz required"), + wx.OK | wx.OK_DEFAULT | wx.CANCEL | wx.ICON_QUESTION, + ) + + val = dlg.ShowModal() + if val == wx.ID_OK: + webbrowser.open(GRAPHVIZ_DOWNLOAD_URL) + + def UpdateAvailable(self, version=None, page=None, required=False): + if required: + flags = wx.OK | wx.OK_DEFAULT | wx.ICON_ERROR + # Translators: The title of a message box shown when a Treemendous update must be downloaded. + title = _("Update required") + if version: + # Translators: Part of a message shown when a Treemendous update is required. + body = _("An update to Treemendous {version} is required.").format( + version=version + ) + else: + # Translators: Part of a message shown when a Treemendous update is required. + body = _("A Treemendous update is required.") + if page: + # Translators: Part of a message shown when a Treemendous update is required. + footer = _( + "The release page will be opened in your web browser so that you can download and install the update." + ) + else: + # Translators: Part of a message shown when a Treemendous update is required. + footer = _("This update must be downloaded and installed manually.") + else: + flags = wx.OK | wx.OK_DEFAULT | wx.CANCEL | wx.ICON_QUESTION + # Translators: The title of a message box shown when a Treemendous update is available for download, but not required. + title = _("Update available") + if version: + # Translators: Part of a message shown when a Treemendous update is available, but not required. + body = _("An update to Treemendous {version} is available.").format( + version=version + ) + else: + # Translators: Part of a message shown when a Treemendous update is available, but not required. + body = _("A Treemendous update is available.") + if page: + # Translators: Part of a message shown when a Treemendous update is available, but not required. + footer = _( + "If you proceed, the release page will be opened in your web browser so that you can download and install the update." + ) + else: + # Translators: Part of a message shown when a Treemendous update is available, but not required. + footer = _("Please manually download and install this update.") + dlg = wx.MessageDialog(self, f"{body}\n{footer}", title, flags) + + val = dlg.ShowModal() + if val == wx.ID_OK: + if page: + webbrowser.open(page) + raise RuntimeError("Update requested, exiting.") + + def OnViewVisual(self, event): + try: + path = self.tree.graphviz(dpi=200) + except GraphvizNotFound: + self.GetGraphviz() + else: + dlg = VisualViewDialog(path, self.platform) + dlg.ShowModal() + dlg.Destroy() + os.remove(path) + + def OnQtree(self, event): + dlg = ReadOnlyViewDialog( + # Translators: The title of a dialog displaying LaTeX source code for the currently opened tree. + _("LaTeX source"), + self.tree.qtree(), + ) + dlg.ShowModal() + dlg.Destroy() + + def OnAbout(self, event): + dlg = wx.MessageDialog( + self, + ( + f"Treemendous {__version__}\n" + "Copyright 2021 Bill Dengler and open-source contributors\n" + "Licensed under the Mozilla Public License, v. 2.0: https://mozilla.org/MPL/2.0/" + ), + "Treemendous", + wx.OK | wx.ICON_INFORMATION, + ) + dlg.ShowModal() + dlg.Destroy() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "path", help="The path of a .treemendous file to open on launch", nargs="?" + ) + parser.add_argument( + "--platform", + help="Override the detected system platform used when drawing the UI (will probably break accessibility, only use for testing)", + choices=("Windows", "Darwin", "Linux"), + ) + args = parser.parse_args() + app = wx.App() + Editor(path=args.path, system=args.platform) + app.MainLoop()