commit cc684dfde2065faeb6d1089a81ff947ddc0b8470 Author: Ivan Holmes Date: Thu Oct 31 00:26:08 2019 +0000 repository created diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da86f19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +# Mac stuff +.DS_Store +.vscode/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8410c20 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ +GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8ec7c4 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# chordsheet diff --git a/chordsheet/components/chordprogression.py b/chordsheet/components/chordprogression.py new file mode 100644 index 0000000..608e123 --- /dev/null +++ b/chordsheet/components/chordprogression.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm +from reportlab.lib.pagesizes import A4 +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.graphics.shapes import * + +from graphics.primitives import * + +class ChordProgression: + def __init__(self, currentCanvas, blockList, **kwargs): + self.currentCanvas = currentCanvas + + self.beatsHeight = kwargs.get('beatsHeight', 5*mm) + self.timeFontSize = kwargs.get('timeFontSize', 12) + self.chordNameFontSize = kwargs.get('chordNameFontSize', 18) + self.notesFontSize = kwargs.get('notesFontSize', 12) + self.unitWidth = kwargs.get('unitWidth', 10*mm) + self.unitHeight = kwargs.get('unitHeight', 20*mm) + + self.timeSignature = kwargs.get('timeSignature', 4) + def draw(self): + global cur_pos, margin, pagesize + writeText(self.currentCanvas, "Chord progression", size=18, align="left") + + v_origin = cur_pos + 2*mm + self.beatsHeight + h_origin = margin + + h_loc = 0 + v_loc = 0 + + maxWidth = int((((pagesize[0]-(2*margin))/self.unitWidth)//(self.timeSignature*2))*(self.timeSignature*2)) # use integer division to round maxWidth to nearest multiple of time signature + + for u in range(maxWidth+1): + s = 0 + x = u*self.unitWidth+margin + if u % self.timeSignature == 0: + e = -self.beatsHeight + else: + e = -self.beatsHeight/2 + drawVertLine(self.currentCanvas, s, e, x, h_origin, v_origin) + if u == maxWidth: # Avoid writing beat number after the final line + break + writeText(str((u % self.timeSignature)+1),size=self.timeFontSize, hpos=x+self.unitWidth/2, vpos=v_origin-self.beatsHeight) + + blockList = parseBlockList(self.blockList, maxWidth) + + for b in blockList: + if h_loc == maxWidth: + v_loc += 1 + h_loc = 0 + currentCanvas.rect(h_origin+(h_loc*self.unitWidth), v_origin+(v_loc*self.unitHeight), b[0]*self.unitWidth, self.unitHeight) + if b[2]: + writeText(currentCanvas, b[2], size=self.notesFontSize, hpos=h_origin+((h_loc+b[0]/2)*self.unitWidth), vpos=v_origin+((v_loc+1)*self.unitHeight)-(1.3*self.notesFontSize)) + v_offset = (v_loc*self.unitHeight+self.unitHeight/2)-self.chordNameFontSize/2 + writeText(currentCanvas, parseName(b[1]), size=self.chordNameFontSize, hpos=h_origin+((h_loc+b[0]/2)*self.unitWidth), vpos=v_origin+v_offset) + h_loc += b[0] + + cur_pos = v_origin+(v_loc+1)*self.unitHeight+self.beatsHeight \ No newline at end of file diff --git a/chordsheet/components/guitarchart.py b/chordsheet/components/guitarchart.py new file mode 100644 index 0000000..28d130a --- /dev/null +++ b/chordsheet/components/guitarchart.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm +from reportlab.lib.pagesizes import A4 +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.graphics.shapes import * + +from graphics.primitives import * + +string_hz_sp = 20*mm +string_hz_gap = 2*mm +string_height = 5*mm + +def guitarChart(currentCanvas, string_hz_sp, string_hz_gap, string_height): + global cur_pos, margin, pagesize + + writeText("Guitar chord voicings", size=18, align="left") + + chartmargin = 15*mm + v_origin = cur_pos + 2*mm + h_origin = margin + chartmargin + nstrings = 6 + fontsize = 12 + + guitarChordList = [[chordList[q].guitar[r] for q in range(len(chordList)) if hasattr(chordList[q], 'guitar')] for r in range(6)] + guitarChordList.append([chordList[q].name for q in range(len(chordList))]) + + for i in range(nstrings+1): # i is the string currently being drawn + writeText(['e','B','G','D','A','E','Name'][i], size=fontsize, hpos=(h_origin), vpos=v_origin+(i*string_height), align='right') + + for j in range(len(ls.chords)): # j is which chord (0 is first chord, 1 is 2nd etc) + if j == 0: + charpos = string_hz_sp/2 + s = string_hz_gap + e = charpos-((c.stringWidth(chordList[i][j])/2)+string_hz_gap) + y = v_origin+(string_height*i)+string_height/2 + drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) + else: + charpos = string_hz_sp*(j+0.5) + s = charpos-string_hz_sp+(lastWidth/2+string_hz_gap) + e = charpos-((currentCanvas.stringWidth(chordList[i][j])/2)+string_hz_gap) + y = v_origin+(string_height*i)+string_height/2 + drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) + if j == len(ls.chords)-1: + s = charpos+(currentCanvas.stringWidth(chordList[i][j])/2+string_hz_gap) + e = charpos+string_hz_sp/2 + y = v_origin+(string_height*i)+string_height/2 + drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) + + writeText(chordList[i][j], size=fontsize, hpos=h_origin+charpos, vpos=v_origin+(i*string_height)) + + lastWidth = currentCanvas.stringWidth(chordList[i][j]) + + cur_pos += (string_height*(nstrings+2)) \ No newline at end of file diff --git a/chordsheet/components/header.py b/chordsheet/components/header.py new file mode 100644 index 0000000..bf2c2fe --- /dev/null +++ b/chordsheet/components/header.py @@ -0,0 +1,4 @@ +def header(): + writeText(ls.title.cdata, size=24) + writeText("Composer: {c}".format(c = ls.composer.cdata), size=12) + writeText("Arranger: {a}".format(a = ls.arranger.cdata), size=12) \ No newline at end of file diff --git a/chordsheet/document.py b/chordsheet/document.py new file mode 100644 index 0000000..03a4968 --- /dev/null +++ b/chordsheet/document.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +from xml.etree import ElementTree as ET +from chordsheet.parsers import parseFingering, parseName +from reportlab.lib.units import mm +from reportlab.lib.pagesizes import A4 + +defaultTimeSignature = 4 + +class Style: + def __init__(self, **kwargs): + self.unit = kwargs.get('unit', mm) + + self.pageSize = kwargs.get('pageSize', A4) + self.leftMargin = kwargs.get('leftMargin', 10) + self.topMargin = kwargs.get('topMargin', 10) + self.font = kwargs.get('font', 'FreeSans') + self.lineSpacing = kwargs.get('lineSpacing', 1.15) + self.separatorSize = kwargs.get('separatorSize', 5) + + self.useIncludedFont = False + + self.stringHzSp = 20*self.unit + self.stringHzGap = 2*self.unit + self.stringHeight = 5*self.unit + + self.unitWidth = 10*self.unit + self.unitHeight = 20*self.unit + self.beatsHeight = 5*self.unit + + self.notesFontSize = 12 + self.chordNameFontSize = 18 + self.beatsFontSize = 12 + +class Chord: + def __init__(self, name, **kwargs): + self.name = name + for inst, fing in kwargs.items(): + setattr(self, inst, fing) + +class Block: + def __init__(self, length, **kwargs): + self.length = length + self.chord = kwargs.get('chord', None) + self.notes = kwargs.get('notes', None) + +class Document: + def __init__(self, chordList=None, blockList=None, title=None, composer=None, arranger=None, timeSignature=defaultTimeSignature): + self.chordList = chordList or [] + self.blockList = blockList or [] + self.title = title or '' # Do not initialise title empty + self.composer = composer + self.arranger = arranger + self.timeSignature = timeSignature + + def loadXML(self, filepath): + xmlDoc = ET.parse(filepath) + root = xmlDoc.getroot() + + self.chordList = [] + if root.find('chords') is not None: + for c in root.findall('chords/chord'): + self.chordList.append(Chord(parseName(c.find('name').text))) + for v in c.findall('voicing'): + setattr(self.chordList[-1], v.attrib['instrument'], + parseFingering(v.text, v.attrib['instrument'])) + + self.blockList = [] + if root.find('progression') is not None: + for b in root.findall('progression/block'): + blockChordName = parseName(b.find('chord').text) if b.find('chord') is not None else None + if blockChordName: + blockChord = None + for c in self.chordList: + if c.name == blockChordName: + blockChord = c + break + if blockChord is None: + exit("Chord {c} does not match any chord in {l}.".format(c=blockChordName, l=self.chordList)) + else: + blockChord = None + blockNotes = (b.find('notes').text if b.find('notes') is not None else None) + self.blockList.append(Block(int(b.find('length').text), chord=blockChord, notes=blockNotes)) + + self.title = (root.find('title').text if root.find('title') is not None else '') # Do not initialise title empty + self.composer = (root.find('composer').text if root.find('composer') is not None else None) + self.arranger = (root.find('arranger').text if root.find('arranger') is not None else None) + self.timeSignature = (int(root.find('timesignature').text) if root.find('timesignature') is not None else defaultTimeSignature) + + def newFromXML(filepath): + doc = Document() + doc.loadXML(filepath) + return doc + + def saveXML(self, filepath): + root = ET.Element("chordsheet") + + ET.SubElement(root, "title").text = self.title + + if self.arranger is not None: + ET.SubElement(root, "arranger").text = self.arranger + + if self.composer is not None: + ET.SubElement(root, "composer").text = self.composer + + ET.SubElement(root, "timesignature").text = str(self.timeSignature) + + chordsElement = ET.SubElement(root, "chords") + + for c in self.chordList: + chordElement = ET.SubElement(chordsElement, "chord") + ET.SubElement(chordElement, "name").text = c.name + if hasattr(c, 'guitar'): + ET.SubElement(chordElement, "voicing", attrib={'instrument':'guitar'}).text = ','.join(c.guitar) + if hasattr(c, 'piano'): + ET.SubElement(chordElement, "voicing", attrib={'instrument':'piano'}).text = c.piano[0] # return first element of list as feature has not been implemented + + progressionElement = ET.SubElement(root, "progression") + + for b in self.blockList: + blockElement = ET.SubElement(progressionElement, "block") + ET.SubElement(blockElement, "length").text = str(b.length) + if b.chord is not None: + ET.SubElement(blockElement, "chord").text = b.chord.name + if b.notes is not None: + ET.SubElement(blockElement, "notes").text = b.notes + + tree = ET.ElementTree(root) + tree.write(filepath) + + + \ No newline at end of file diff --git a/chordsheet/parsers.py b/chordsheet/parsers.py new file mode 100644 index 0000000..12b1bd2 --- /dev/null +++ b/chordsheet/parsers.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from sys import exit + +def parseFingering(fingering, instrument): + if instrument == 'guitar': + numStrings = 6 + if len(fingering) == numStrings: + output = list(fingering) + else: + output = [x for x in fingering.split(',')] + if len(output) == numStrings: + return output + else: + exit("Voicing <{v}> is malformed.".format(v=fingering)) + else: + return [fingering] + +nameReplacements = { "b":"♭", "#":"♯" } + +def parseName(chordName): + parsedName = chordName + for i, j in nameReplacements.items(): + parsedName = parsedName.replace(i, j) + return parsedName \ No newline at end of file diff --git a/chordsheet/primitives.py b/chordsheet/primitives.py new file mode 100644 index 0000000..9c65490 --- /dev/null +++ b/chordsheet/primitives.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm +from reportlab.graphics.shapes import * + +def writeText(currentCanvas, style, string, size, vpos, **kwargs): + margin = style.leftMargin*style.unit + + align = kwargs.get('align', 'centre') + if align == 'centre' or align == 'center': + hpos = kwargs.get('hpos', style.pageSize[0]/2) + elif align == 'left': + hpos = kwargs.get('hpos', margin) + elif align == 'right': + hpos = kwargs.get('hpos', style.pageSize[0]-margin) + spacing = kwargs.get('spacing', style.lineSpacing) + + currentCanvas.setFont(style.font, size) + + if align == 'centre' or align == 'center': + currentCanvas.drawCentredString(hpos, vpos+(0.75*size*spacing),string) + elif align == 'left': + currentCanvas.drawString(hpos, vpos+(0.75*size*spacing),string) + elif align == 'right': + currentCanvas.drawString(hpos-currentCanvas.stringWidth(string), vpos+(0.75*size*spacing),string) + + return size*style.lineSpacing + +def drawHorizLine(currentCanvas, startpoint, endpoint, v_pos, h_origin, v_origin): + x1 = h_origin+startpoint + x2 = h_origin+endpoint + currentCanvas.line(x1, v_pos, x2, v_pos) + +def drawVertLine(currentCanvas, startpoint, endpoint, h_pos, h_origin, v_origin): + y1 = v_origin+startpoint + y2 = v_origin+endpoint + currentCanvas.line(h_pos, y1, h_pos, y2) + \ No newline at end of file diff --git a/chordsheet/render.py b/chordsheet/render.py new file mode 100644 index 0000000..cfc9a69 --- /dev/null +++ b/chordsheet/render.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- + +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm +from reportlab.graphics.shapes import * +from chordsheet.primitives import writeText, drawVertLine, drawHorizLine +from chordsheet.document import Block + +def splitBlocks(blockList, maxWidth): + h_loc = 0 + splitBlockList = [] + for i in range(len(blockList)): + c_orig = blockList[i].chord + n_orig = blockList[i].notes + if h_loc == maxWidth: + h_loc = 0 + if h_loc+blockList[i].length > maxWidth: + lengthList = [maxWidth - h_loc] + while sum(lengthList) < blockList[i].length: + if blockList[i].length - sum(lengthList) >= maxWidth: + lengthList.append(maxWidth) + else: + lengthList.append(blockList[i].length - sum(lengthList)) + + for l in lengthList: + splitBlockList.append(Block(l, chord=c_orig, notes=n_orig)) # create a block with the given length + + h_loc = lengthList[-1] + else: + splitBlockList.append(blockList[i]) + h_loc += blockList[i].length + return splitBlockList + +def guitarChart(currentCanvas, style, chordList, cur_pos): + title_height = writeText(currentCanvas, style, "Guitar chord voicings", 18, cur_pos, align="left") + cur_pos += title_height + + string_hz_sp = style.stringHzSp + string_hz_gap = style.stringHzGap + string_height = style.stringHeight + + margin = style.leftMargin*style.unit + pagesize = style.pageSize + + chartmargin = 15*mm + v_origin = cur_pos + 2*mm + h_origin = margin + chartmargin + nstrings = 6 + fontsize = 12 + + guitarChordList = [[chordList[q].guitar[-(r+1)] for q in range(len(chordList)) if hasattr(chordList[q], 'guitar')] for r in range(6)] + guitarChordList.append([chordList[q].name for q in range(len(chordList)) if hasattr(chordList[q], 'guitar')]) + + for i in range(nstrings+1): # i is the string currently being drawn + writeText(currentCanvas, style, ['e','B','G','D','A','E','Name'][i], fontsize, v_origin+(i*string_height), hpos=h_origin, align='right') + + for j in range(len(guitarChordList[-1])): # j is which chord (0 is first chord, 1 is 2nd etc) + if j == 0: + charpos = string_hz_sp/2 + s = string_hz_gap + e = charpos-((currentCanvas.stringWidth(guitarChordList[i][j])/2)+string_hz_gap) + y = v_origin+(string_height*i)+string_height/2 + drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) + else: + charpos = string_hz_sp*(j+0.5) + s = charpos-string_hz_sp+(lastWidth/2+string_hz_gap) + e = charpos-((currentCanvas.stringWidth(guitarChordList[i][j])/2)+string_hz_gap) + y = v_origin+(string_height*i)+string_height/2 + drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) + + if j == len(guitarChordList[-1])-1: + s = charpos+(currentCanvas.stringWidth(guitarChordList[i][j])/2+string_hz_gap) + e = charpos+string_hz_sp/2 + y = v_origin+(string_height*i)+string_height/2 + drawHorizLine(currentCanvas, s, e, y, h_origin, v_origin) + + writeText(currentCanvas, style, guitarChordList[i][j], fontsize, v_origin+(i*string_height), hpos=h_origin+charpos) + + lastWidth = currentCanvas.stringWidth(guitarChordList[i][j]) + + return (string_height*(nstrings+1)) + title_height # calculate the height of the block + +def chordProgression(currentCanvas, style, document, cur_pos): + margin = style.leftMargin*style.unit + pagesize = style.pageSize + + title_height = writeText(currentCanvas, style, "Chord progression", 18, cur_pos, align="left") + cur_pos += title_height + + v_origin = cur_pos + 2*mm + style.beatsHeight + h_origin = margin + + h_loc = 0 + v_loc = 0 + + maxWidth = int((((pagesize[0]-(2*margin))/style.unitWidth)//(document.timeSignature*2))*(document.timeSignature*2)) # use integer division to round maxWidth to nearest two bars + + for u in range(maxWidth+1): + s = 0 + x = u*style.unitWidth+margin + if u % document.timeSignature == 0: + e = -style.beatsHeight + else: + e = -style.beatsHeight/2 + drawVertLine(currentCanvas, s, e, x, h_origin, v_origin) + if u == maxWidth: # Avoid writing beat number after the final line + break + writeText(currentCanvas, style, str((u % document.timeSignature)+1), style.beatsFontSize, v_origin-style.beatsHeight, hpos=x+style.unitWidth/2) + + parsedBlockList = splitBlocks(document.blockList, maxWidth) + + for b in parsedBlockList: + if h_loc == maxWidth: + v_loc += 1 + h_loc = 0 + currentCanvas.rect(h_origin+(h_loc*style.unitWidth), v_origin+(v_loc*style.unitHeight), b.length*style.unitWidth, style.unitHeight) + if b.notes is not None: + writeText(currentCanvas, style, b.notes, style.notesFontSize, v_origin+((v_loc+1)*style.unitHeight)-(1.3*style.notesFontSize), hpos=h_origin+((h_loc+b.length/2)*style.unitWidth)) + v_offset = ((v_loc*style.unitHeight)+style.unitHeight/2)-style.chordNameFontSize/2 + if b.chord is not None: + writeText(currentCanvas, style, b.chord.name, style.chordNameFontSize, v_origin+v_offset, hpos=h_origin+((h_loc+b.length/2)*style.unitWidth)) + h_loc += b.length + + return v_origin + (v_loc+1)*style.unitHeight + style.beatsHeight + title_height # calculate the height of the generated chart + +def guitarChartCheck(cL): + chordsPresent = False + for c in cL: + if hasattr(c, 'guitar'): + chordsPresent = True + break + return chordsPresent + +def savePDF(document, style, pathToPDF): + + c = canvas.Canvas(pathToPDF, pagesize=style.pageSize, bottomup=0) + + curPos = style.topMargin*style.unit + + if document.title is not None: + curPos += writeText(c, style, document.title, 24, curPos) + + if document.composer is not None: + curPos += writeText(c, style, "Composer: {c}".format(c = document.composer), 12, curPos) + + if document.arranger is not None: + curPos += writeText(c, style, "Arranger: {a}".format(a = document.arranger), 12, curPos) + + curPos += style.separatorSize*style.unit + + if guitarChartCheck(document.chordList): + curPos += guitarChart(c, style, document.chordList, curPos) + + curPos += style.separatorSize*style.unit + + if document.blockList: + curPos += chordProgression(c, style, document, curPos) + + curPos += style.separatorSize*style.unit + + c.save() \ No newline at end of file diff --git a/chordsheet/tableView.py b/chordsheet/tableView.py new file mode 100644 index 0000000..1e1afd7 --- /dev/null +++ b/chordsheet/tableView.py @@ -0,0 +1,78 @@ +import sys +from PyQt5 import QtWidgets, QtGui, QtCore + +class MItemModel(QtGui.QStandardItemModel): + + def dropMimeData(self, data, action, row, col, parent): + """ + Always move the entire row, and don't allow column "shifting" + """ + return super().dropMimeData(data, action, row, 0, parent) + +class MProxyStyle(QtWidgets.QProxyStyle): + + def drawPrimitive(self, element, option, painter, widget=None): + """ + Draw a line across the entire row rather than just the column + we're hovering over. This may not always work depending on global + style. + """ + if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull(): + option_new = QtWidgets.QStyleOption(option) + option_new.rect.setLeft(0) + if widget: + option_new.rect.setRight(widget.width()) + option = option_new + super().drawPrimitive(element, option, painter, widget) + +class MTableView(QtWidgets.QTableView): + + def __init__(self, parent): + super().__init__(parent) + + self.verticalHeader().hide() + self.horizontalHeader().show() + self.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) + self.setShowGrid(False) + #self.setDragDropMode(self.InternalMove) + #self.setDragDropOverwriteMode(False) + + # Set our custom style - this draws the drop indicator across the whole row + self.setStyle(MProxyStyle()) + + self.model = MItemModel() + self.setModel(self.model) + +class ChordTableView(MTableView): + + def __init__(self, parent): + super().__init__(parent) + + self.model.setHorizontalHeaderLabels(['Chord', 'Voicing']) + + def populate(self, cList): + self.model.removeRows(0, self.model.rowCount()) + for c in cList: + rowList = [QtGui.QStandardItem(c.name), QtGui.QStandardItem(",".join(c.guitar if hasattr(c, 'guitar') else ""))] + for item in rowList: + item.setEditable(False) + item.setDropEnabled(False) + + self.model.appendRow(rowList) + +class BlockTableView(MTableView): + + def __init__(self, parent): + super().__init__(parent) + + self.model.setHorizontalHeaderLabels(['Chord', 'Length', 'Notes']) + + def populate(self, bList): + self.model.removeRows(0, self.model.rowCount()) + for b in bList: + rowList = [QtGui.QStandardItem(b.chord.name), QtGui.QStandardItem(str(b.length)), QtGui.QStandardItem(b.notes)] + for item in rowList: + item.setEditable(False) + item.setDropEnabled(False) + + self.model.appendRow(rowList) \ No newline at end of file diff --git a/examples/ah.xml b/examples/ah.xml new file mode 100644 index 0000000..255932c --- /dev/null +++ b/examples/ah.xml @@ -0,0 +1,75 @@ + + "African Heritage" + Ivan Holmes + Ivan Holmes and Joe Buckley + 6 + + + Gm9 + x,10,8,10,10,x + + + Abm9 + x,11,9,11,11,x + + + Cm9 + x,x,8,8,8,10 + + + D7#5#9 + x58565 + + + Eb7#5#9 + x69676 + + + + + 9 + Gm9 + + + 3 + Abm9 + + + 12 + Gm9 + + + 9 + Cm9 + + + 3 + D7#5#9 + + + 12 + Gm9 + + + 6 + Eb7#5#9 + + + 6 + D7#5#9 + + + 6 + Cm9 + + + 3 + D7#5#9 + + + 3 + D7#5#9 + over Ab + + + \ No newline at end of file diff --git a/examples/example.xml b/examples/example.xml new file mode 100644 index 0000000..aa4d0f4 --- /dev/null +++ b/examples/example.xml @@ -0,0 +1,46 @@ + + Composition + A. Person + 4 + + + B + xx2341 + abcdefg + + + E + 022100 + + + Cm9 + x,x,8,8,8,10 + + + D7b5#9 + + + + + 4 + B + These are notes + + + 4 + E + + + 12 + Cm9 + + + 6 + D7b5#9 + + + 6 + For quiet contemplation. + + + \ No newline at end of file diff --git a/fonts/FreeSans.ttf b/fonts/FreeSans.ttf new file mode 100644 index 0000000..9db9585 Binary files /dev/null and b/fonts/FreeSans.ttf differ diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..512b864 --- /dev/null +++ b/gui.py @@ -0,0 +1,360 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Wed May 29 00:02:24 2019 + +@author: ivan +""" + +import sys, fitz, io, subprocess, os + +from PyQt5.QtWidgets import QApplication, QAction, QLabel, QDialogButtonBox, QDialog, QFileDialog, QMessageBox, QPushButton, QLineEdit, QCheckBox, QSpinBox, QDoubleSpinBox, QTableWidget, QTableWidgetItem, QTabWidget, QComboBox, QWidget, QScrollArea +from PyQt5.QtCore import QFile, QObject +from PyQt5.QtGui import QPixmap, QImage +from PyQt5 import uic +from chordsheet.tableView import ChordTableView, BlockTableView , MItemModel, MProxyStyle + +from reportlab.lib.units import mm, cm, inch, pica +from reportlab.lib.pagesizes import A4, A5, LETTER, LEGAL +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + +from chordsheet.document import Document, Style, Chord, Block +from chordsheet.render import savePDF +from chordsheet.parsers import parseFingering, parseName + +if getattr(sys, 'frozen', False): + scriptDir = sys._MEIPASS +else: + scriptDir = os.path.abspath(os.path.dirname(os.path.abspath(__file__))) + +pdfmetrics.registerFont(TTFont('FreeSans', os.path.join(scriptDir, 'fonts', 'FreeSans.ttf'))) +if sys.platform == "darwin": + pdfmetrics.registerFont(TTFont('HelveticaNeue', 'HelveticaNeue.ttc', subfontIndex=0)) + +pageSizeDict = {'A4':A4, 'A5':A5, 'Letter':LETTER, 'Legal':LEGAL} +unitDict = {'mm':mm, 'cm':cm, 'inch':inch, 'point':1, 'pica':pica} # point is 1 because reportlab's native unit is points. + +class DocumentWindow(QWidget): + def __init__(self, doc, style, parent=None): + super().__init__(parent) + + self.doc = doc + self.style = style + + self.UIFileLoader(str(os.path.join(scriptDir, 'ui','mainwindow.ui'))) + self.UIInitStyle() + # self.UIInitDocument() + + def UIFileLoader(self, ui_file): + ui_file = QFile(ui_file) + ui_file.open(QFile.ReadOnly) + + self.window = uic.loadUi(ui_file) + ui_file.close() + + self.window.actionNew.triggered.connect(self.menuFileNewAction) + self.window.actionOpen.triggered.connect(self.menuFileOpenAction) + self.window.actionSave.triggered.connect(self.menuFileSaveAction) + self.window.actionSave_as.triggered.connect(self.menuFileSaveAsAction) + self.window.actionSave_PDF.triggered.connect(self.menuFileSavePDFAction) + self.window.actionPrint.triggered.connect(self.menuFilePrintAction) + self.window.actionClose.triggered.connect(self.menuFileCloseAction) + + self.window.pageSizeComboBox.currentIndexChanged.connect(self.pageSizeAction) + self.window.documentUnitsComboBox.currentIndexChanged.connect(self.unitAction) + + self.window.includedFontCheckBox.stateChanged.connect(self.includedFontAction) + + self.window.generateButton.clicked.connect(self.generateAction) + + self.window.guitarVoicingButton.clicked.connect(self.guitarVoicingAction) + self.window.addChordButton.clicked.connect(self.addChordAction) + self.window.removeChordButton.clicked.connect(self.removeChordAction) + self.window.updateChordButton.clicked.connect(self.updateChordAction) + + self.window.addBlockButton.clicked.connect(self.addBlockAction) + self.window.removeBlockButton.clicked.connect(self.removeBlockAction) + self.window.updateBlockButton.clicked.connect(self.updateBlockAction) + + self.window.chordTableView.clicked.connect(self.chordClickedAction) + self.window.blockTableView.clicked.connect(self.blockClickedAction) + + def UIInitDocument(self): + + self.window.titleLineEdit.setText(self.doc.title) + self.window.composerLineEdit.setText(self.doc.composer) + self.window.arrangerLineEdit.setText(self.doc.arranger) + self.window.timeSignatureSpinBox.setValue(self.doc.timeSignature) + + # chord and block table lists here + self.window.chordTableView.populate(self.doc.chordList) + self.window.blockTableView.populate(self.doc.blockList) + self.updateChordDict() + + self.window.tabWidget.setCurrentWidget(self.window.tabWidget.findChild(QWidget, 'Overview')) + # self.updatePreview() + + def UIInitStyle(self): + self.window.pageSizeComboBox.addItems(list(pageSizeDict.keys())) + self.window.pageSizeComboBox.setCurrentText(list(pageSizeDict.keys())[0]) + + self.window.documentUnitsComboBox.addItems(list(unitDict.keys())) + self.window.documentUnitsComboBox.setCurrentText(list(unitDict.keys())[0]) + + self.window.lineSpacingDoubleSpinBox.setValue(self.style.lineSpacing) + + self.window.leftMarginLineEdit.setText(str(self.style.leftMargin)) + self.window.topMarginLineEdit.setText(str(self.style.topMargin)) + + self.window.fontComboBox.setDisabled(True) + self.window.includedFontCheckBox.setChecked(True) + + def pageSizeAction(self, index): + self.pageSizeSelected = self.window.pageSizeComboBox.itemText(index) + + def unitAction(self, index): + self.unitSelected = self.window.documentUnitsComboBox.itemText(index) + + def includedFontAction(self): + if self.window.includedFontCheckBox.isChecked(): + self.style.useIncludedFont = True + else: + self.style.useIncludedFont = False + + def chordClickedAction(self, index): + self.window.chordNameLineEdit.setText(self.window.chordTableView.model.item(index.row(), 0).text()) + self.window.guitarVoicingLineEdit.setText(self.window.chordTableView.model.item(index.row(), 1).text()) + + def blockClickedAction(self, index): + self.window.blockChordComboBox.setCurrentText(self.window.blockTableView.model.item(index.row(), 0).text()) + self.window.blockLengthLineEdit.setText(self.window.blockTableView.model.item(index.row(), 1).text()) + self.window.blockNotesLineEdit.setText(self.window.blockTableView.model.item(index.row(), 2).text()) + + def menuFileNewAction(self): + self.doc = Document() + + def menuFileOpenAction(self): + filePath = QFileDialog.getOpenFileName(self.window.tabWidget, 'Open file', str(os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") + if filePath[0]: + self.currentFilePath = filePath[0] + self.doc.loadXML(filePath[0]) + self.UIInitDocument() + self.updatePreview() + + def menuFileSaveAction(self): + self.updateDocument() + if not (hasattr(self, 'currentFilePath') and self.currentFilePath): + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', str(os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") + self.currentFilePath = filePath[0] + self.doc.saveXML(self.currentFilePath) + + def menuFileSaveAsAction(self): + self.updateDocument() + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', str(os.path.expanduser("~")), "Chordsheet ML files (*.xml *.cml)") + if filePath[0]: + self.currentFilePath = filePath[0] + self.doc.saveXML(self.currentFilePath) + + def menuFileSavePDFAction(self): + self.updateDocument() + self.updatePreview() + filePath = QFileDialog.getSaveFileName(self.window.tabWidget, 'Save file', str(os.path.expanduser("~")), "PDF files (*.pdf)") + if filePath[0]: + savePDF(d, s, filePath[0]) + + def menuFilePrintAction(self): + if sys.platform == "darwin": + pass + # subprocess.call() + else: + pass + + def menuFileCloseAction(self): + pass + + def guitarVoicingAction(self): + gdialog = GuitarDialog() + + voicing = gdialog.getVoicing() + if voicing: + self.window.guitarVoicingLineEdit.setText(voicing) + + def clearChordLineEdits(self): + self.window.chordNameLineEdit.clear() + self.window.guitarVoicingLineEdit.clear() + self.window.chordNameLineEdit.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown) + self.window.guitarVoicingLineEdit.clear() + + def updateChordDict(self): + self.chordDict = {c.name:c for c in self.doc.chordList} + self.window.blockChordComboBox.clear() + self.window.blockChordComboBox.addItems(list(self.chordDict.keys())) + + def removeChordAction(self): + self.updateChords() + + row = self.window.chordTableView.selectionModel().currentIndex().row() + self.doc.chordList.pop(row) + + self.window.chordTableView.populate(self.doc.chordList) + self.clearChordLineEdits() + self.updateChordDict() + + def addChordAction(self): + self.updateChords() + + self.doc.chordList.append(Chord(parseName(self.window.chordNameLineEdit.text()))) + if self.window.guitarVoicingLineEdit.text(): + setattr(self.doc.chordList[-1], 'guitar', parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar')) + + self.window.chordTableView.populate(self.doc.chordList) + self.clearChordLineEdits() + self.updateChordDict() + + def updateChordAction(self): + if self.window.chordTableView.selectionModel().hasSelection(): + self.updateChords() + row = self.window.chordTableView.selectionModel().currentIndex().row() + self.doc.chordList[row] = Chord(parseName(self.window.chordNameLineEdit.text())) + if self.window.guitarVoicingLineEdit.text(): + setattr(self.doc.chordList[row], 'guitar', parseFingering(self.window.guitarVoicingLineEdit.text(), 'guitar')) + + self.window.chordTableView.populate(self.doc.chordList) + self.clearChordLineEdits() + self.updateChordDict() + + def clearBlockLineEdits(self): + self.window.blockLengthLineEdit.clear() + self.window.blockNotesLineEdit.clear() + self.window.blockLengthLineEdit.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown) + self.window.blockNotesLineEdit.repaint() + + def removeBlockAction(self): + self.updateBlocks() + + row = self.window.blockTableView.selectionModel().currentIndex().row() + self.doc.blockList.pop(row) + + self.window.blockTableView.populate(self.doc.blockList) + + def addBlockAction(self): + self.updateBlocks() + + self.doc.blockList.append(Block(self.window.blockLengthLineEdit.text(), + chord = self.chordDict[self.window.blockChordComboBox.currentText()], + notes = (self.window.blockNotesLineEdit.text() if not "" else None))) + + self.window.blockTableView.populate(self.doc.blockList) + self.clearBlockLineEdits() + + def updateBlockAction(self): + if self.window.blockTableView.selectionModel().hasSelection(): + self.updateBlocks() + row = self.window.blockTableView.selectionModel().currentIndex().row() + self.doc.blockList[row] = (Block(self.window.blockLengthLineEdit.text(), + chord = self.chordDict[self.window.blockChordComboBox.currentText()], + notes = (self.window.blockNotesLineEdit.text() if not "" else None))) + + self.window.blockTableView.populate(self.doc.blockList) + self.clearBlockLineEdits() + + def generateAction(self): + self.updateDocument() + self.updatePreview() + + def updatePreview(self): + self.currentPreview = io.BytesIO() + savePDF(self.doc, self.style, self.currentPreview) + + pdfView = fitz.Document(stream=self.currentPreview, filetype='pdf') + pix = pdfView[0].getPixmap(alpha = False) + + fmt = QImage.Format_RGB888 + qtimg = QImage(pix.samples, pix.width, pix.height, pix.stride, fmt) + + self.window.imageLabel.setPixmap(QPixmap.fromImage(qtimg)) + self.window.imageLabel.repaint() # necessary on Mojave with PyInstaller (or previous contents will be shown) + + def updateChords(self): + chordTableList = [] + for i in range(self.window.chordTableView.model.rowCount()): + chordTableList.append(Chord(parseName(self.window.chordTableView.model.item(i, 0).text()))), + if self.window.chordTableView.model.item(i, 1).text(): + chordTableList[-1].guitar = parseFingering(self.window.chordTableView.model.item(i, 1).text(), 'guitar') + + self.doc.chordList = chordTableList + + def updateBlocks(self): + blockTableList = [] + for i in range(self.window.blockTableView.model.rowCount()): + blockLength = int(self.window.blockTableView.model.item(i, 1).text()) + blockChordName = parseName(self.window.blockTableView.model.item(i, 0).text()) + if blockChordName: + blockChord = None + for c in self.doc.chordList: + if c.name == blockChordName: + blockChord = c + break + if blockChord is None: + exit("Chord {c} does not match any chord in {l}.".format(c=blockChordName, l=self.doc.chordList)) + else: + blockChord = None + blockNotes = self.window.blockTableView.model.item(i, 2).text() if self.window.blockTableView.model.item(i, 2).text() else None + blockTableList.append(Block(blockLength, chord=blockChord, notes=blockNotes)) + + self.doc.blockList = blockTableList + + def updateDocument(self): + self.doc.title = self.window.titleLineEdit.text() # Title can be empty string but not None + self.doc.composer = (self.window.composerLineEdit.text() if self.window.composerLineEdit.text() else None) + self.doc.arranger = (self.window.arrangerLineEdit.text() if self.window.arrangerLineEdit.text() else None) + + self.doc.timeSignature = int(self.window.timeSignatureSpinBox.value()) + self.style.pageSize = pageSizeDict[self.pageSizeSelected] + self.style.unit = unitDict[self.unitSelected] + self.style.leftMargin = int(self.window.leftMarginLineEdit.text()) + self.style.topMargin = int(self.window.topMarginLineEdit.text()) + self.style.lineSpacing = float(self.window.lineSpacingDoubleSpinBox.value()) + + self.updateChords() + self.updateBlocks() + + self.style.font = ('FreeSans' if self.style.useIncludedFont else 'HelveticaNeue') + # something for the font box here + +class GuitarDialog(QDialog): + def __init__(self): + super().__init__() + self.UIFileLoader(str(os.path.join(scriptDir, 'ui','guitardialog.ui'))) + + def UIFileLoader(self, ui_file): + ui_file = QFile(ui_file) + ui_file.open(QFile.ReadOnly) + + self.dialog = uic.loadUi(ui_file) + ui_file.close() + + def getVoicing(self): + if self.dialog.exec_() == QDialog.Accepted: + result = [self.dialog.ELineEdit.text(), + self.dialog.ALineEdit.text(), + self.dialog.DLineEdit.text(), + self.dialog.GLineEdit.text(), + self.dialog.BLineEdit.text(), + self.dialog.eLineEdit.text()] + resultJoined = ",".join(result) + return resultJoined + else: + return None + +if __name__ == '__main__': + app = QApplication(sys.argv) + + d = Document() + s = Style() + + w = DocumentWindow(d, s) + w.window.show() + + sys.exit(app.exec_()) diff --git a/gui.spec b/gui.spec new file mode 100644 index 0000000..26f654a --- /dev/null +++ b/gui.spec @@ -0,0 +1,45 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['/Users/ivan/Code/chordsheet/gui.py'], + pathex=['/Users/ivan/Code/chordsheet'], + binaries=[], + datas=[ + ('fonts', 'fonts'), + ('ui', 'ui') + ], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='Chordsheet', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False ) +app = BUNDLE(exe, + name='Chordsheet.app', + icon=None, + bundle_identifier=None, + info_plist={ + 'NSPrincipalClass': 'NSApplication', + 'NSHighResolutionCapable': 'True' + } + ) diff --git a/ui/guitardialog.ui b/ui/guitardialog.ui new file mode 100644 index 0000000..68c1c78 --- /dev/null +++ b/ui/guitardialog.ui @@ -0,0 +1,182 @@ + + + Dialog + + + + 0 + 0 + 300 + 111 + + + + + 300 + 111 + + + + + 300 + 111 + + + + Guitar chord + + + + + + + + G + + + + + + + E + + + + + + + B + + + + + + + A + + + + + + + D + + + + + + + e + + + + + + + + 0 + 21 + + + + + + + + + 0 + 21 + + + + + + + + + 0 + 21 + + + + + + + + + 0 + 21 + + + + + + + + + 0 + 21 + + + + + + + + + 0 + 21 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui new file mode 100644 index 0000000..029b78b --- /dev/null +++ b/ui/mainwindow.ui @@ -0,0 +1,895 @@ + + + MainWindow + + + + 0 + 0 + 1113 + 746 + + + + Chordsheet + + + + + + + QFrame::NoFrame + + + Qt::Horizontal + + + + + 0 + 0 + + + + + 320 + 0 + + + + + 500 + 16777215 + + + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + 300 + 400 + + + + + 500 + 16777215 + + + + 2 + + + + Overview + + + + + + + + QFormLayout::ExpandingFieldsGrow + + + + + Title + + + + + + + + 0 + 0 + + + + + + + + Composer + + + + + + + + 0 + 0 + + + + + + + + Arranger + + + + + + + + 0 + 0 + + + + + + + + + + + + Time signature + + + + + + + + 40 + 16777215 + + + + + + + 4 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Page + + + + + + Page options + + + + + + + + Page size + + + + + + + + + + Document units + + + + + + + + + + Left margin + + + + + + + + 60 + 16777215 + + + + + + + + Top margin + + + + + + + + 60 + 16777215 + + + + + + + + + + + + + Text options + + + + + + + + Line spacing + + + + + + + + 70 + 0 + + + + + 70 + 16777215 + + + + + + + + + + + + + + 0 + 0 + + + + Font options + + + + + + + + + 40 + 16777215 + + + + Font + + + + + + + + 0 + 0 + + + + + + + + + + + + + Use included FreeSans + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Chords + + + + + + + + + 0 + 0 + + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::IgnoreAction + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + false + + + + + + + + + + 0 + 0 + + + + + 100 + 16777215 + + + + + + + + + 16777215 + 16777215 + + + + Editor... + + + + + + + + 0 + 0 + + + + + + + + Guitar voicing + + + + + + + Chord name + + + + + + + + + + + Remove chord + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Update chord + + + + + + + Add chord + + + + + + + + + + + + Blocks + + + + + + + + + 0 + 0 + + + + + 0 + 200 + + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + QAbstractItemView::InternalMove + + + Qt::TargetMoveAction + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + false + + + + + + + + + Length + + + + + + + + + + + 0 + 0 + + + + Notes + + + + + + + Chord + + + + + + + + 0 + 0 + + + + + 40 + 16777215 + + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Remove block + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Update block + + + + + + + Add block + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + Generate chordsheet + + + + + + + Qt::Horizontal + + + QSizePolicy::MinimumExpanding + + + + 40 + 20 + + + + + + + + + + + + + + 300 + 400 + + + + true + + + + + 0 + 0 + 580 + 698 + + + + true + + + + 12 + + + 12 + + + 12 + + + 12 + + + + + true + + + + + + false + + + 0 + + + + + + + + + + + + + + 0 + 0 + 1113 + 22 + + + + + File + + + + + + + + + + + + + + + + + New... + + + + + Open... + + + + + Save + + + + + Save PDF... + + + + + Print... + + + + + Close + + + + + Cut + + + + + Copy + + + + + Paste + + + + + Save as... + + + + + + ChordTableView + QTableView +
chordsheet/tableView.h
+
+ + BlockTableView + QTableView +
chordsheet/tableView.h
+
+
+ + generateButton + tabWidget + titleLineEdit + composerLineEdit + arrangerLineEdit + timeSignatureSpinBox + pageSizeComboBox + documentUnitsComboBox + leftMarginLineEdit + topMarginLineEdit + lineSpacingDoubleSpinBox + fontComboBox + includedFontCheckBox + chordTableView + chordNameLineEdit + guitarVoicingLineEdit + guitarVoicingButton + addChordButton + removeChordButton + blockTableView + addBlockButton + removeBlockButton + scrollArea + + + +