Commit 0c5880f
Initial commit
Changed files (122)
addedCOPYINGaddedREADMEaddedicons/add_background_image.pngaddedicons/add_new_grid.pngaddedicons/available_characters.pngaddedicons/background_help_lines.pngaddedicons/cancel_save.pngaddedicons/close_paths.pngaddedicons/copy_path.pngaddedicons/corner.pngaddedicons/custom_character_set.pngaddedicons/cut.pngaddedicons/cut_background.pngaddedicons/delete_grid.pngaddedicons/delete_selected_glyph.pngaddedicons/edit_grid.pngaddedicons/erase_tool.pngaddedicons/export.pngaddedicons/file_selection.pngaddedicons/full_glyph.pngaddedicons/help_lines.pngaddedicons/insert_background.pngaddedicons/kerning_context.pngaddedicons/move.pngaddedicons/move_background.pngaddedicons/move_handle.pngaddedicons/move_handle_active.pngaddedicons/move_layer.pngaddedicons/move_point.pngaddedicons/new_file.pngaddedicons/new_grid.pngaddedicons/new_point.pngaddedicons/new_point_on_path.pngaddedicons/next.pngaddedicons/no_icon.pngaddedicons/open.old.pngaddedicons/open.pngaddedicons/paste_path.pngaddedicons/path2826.pngaddedicons/path2863.pngaddedicons/pen_tool.pngaddedicons/prev.pngaddedicons/rect4228.pngaddedicons/resize.pngaddedicons/reverse_path.pngaddedicons/rotate_icon.pngaddedicons/rotate_icon_active.pngaddedicons/save.pngaddedicons/show_background.pngaddedicons/show_xheight_helplines.pngaddedicons/show_xheight_helplines_ (kopia).pngaddedicons/shrink.pngaddedicons/shrink_tool.pngaddedicons/size_to_boundries.pngaddedicons/slow_test.pngaddedicons/spin_button.pngaddedicons/test_case.pngaddedicons/test_icon.pngaddedicons/tie_point.pngaddedicons/tie_x_or_y.pngaddedicons/union_paths.pngaddedicons/utf_8.pngaddedicons/version.pngaddedicons/view_result.pngaddedicons/zoom_1_1.pngaddedicons/zoom_background_image.pngaddedicons/zoom_boundries.pngaddedicons/zoom_in.pngaddedicons/zoom_out.pngaddedicons/zoom_tool.pngaddedsrc/BackgroundSelection.valaaddedsrc/BackgroundTool.valaaddedsrc/CharDatabase.valaaddedsrc/ClipTool.valaaddedsrc/ContentDisplay.valaaddedsrc/Context.valaaddedsrc/CutBackgroundTool.valaaddedsrc/CutTool.valaaddedsrc/DropMenu.valaaddedsrc/EditPoint.valaaddedsrc/EditPointHandle.valaaddedsrc/ExportTool.valaaddedsrc/FileSelection.valaaddedsrc/Font.valaaddedsrc/FontDisplay.valaaddedsrc/Glyph.valaaddedsrc/GlyphBackgroundImage.valaaddedsrc/GlyphCanvas.valaaddedsrc/GlyphCollection.valaaddedsrc/GlyphRange.valaaddedsrc/GridTool.valaaddedsrc/IOThread.valaaddedsrc/Icons.valaaddedsrc/Kerning.valaaddedsrc/KeyBindings.valaaddedsrc/Line.valaaddedsrc/MainWindow.valaaddedsrc/MergeTool.valaaddedsrc/MoveTool.valaaddedsrc/OpenFontFormatWriter.valaaddedsrc/OverView.valaaddedsrc/Path.valaaddedsrc/PenTool.valaaddedsrc/Preferences.valaaddedsrc/ResizeTool.valaaddedsrc/SaveDialog.valaaddedsrc/ShrinkTool.valaaddedsrc/SpinButton.valaaddedsrc/Supplement.valaaddedsrc/Svg.valaaddedsrc/SvgFontFormatWriter.valaaddedsrc/TabBar.valaaddedsrc/TestSupplement.valaaddedsrc/Tool.valaaddedsrc/Toolbox.valaaddedsrc/TooltipArea.valaaddedsrc/UserActions.valaaddedsrc/VersionList.valaaddedsrc/ZoomTool.valaaddedsrc/wscriptaddedwafaddedwscript
Diff
new file mode 100644@@ -0,0 +1,241 @@+GNU GENERAL PUBLIC LICENSE++Version 3, 29 June 2007++Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/>++Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.++Preamble++The GNU General Public License is a free, copyleft license for software and other kinds of works.++The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is 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. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.++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.++To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.++For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.++Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.++For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.++Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.++Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.++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 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. Use with the GNU Affero General Public License.++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 Affero 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 special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.++14. Revised Versions of this License.++The Free Software Foundation may publish revised and/or new versions of the GNU 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 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 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 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.++ <one line to give the program's name and a brief idea of what it does.>+ Copyright (C) <year> <name of author>++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+Also add information on how to contact you by electronic and paper mail.++If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:++ <program> Copyright (C) <year> <name of author>+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.+ This is free software, and you are welcome to redistribute it+ under certain conditions; type `show c' for details.+The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.++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 GPL, see <http://www.gnu.org/licenses/>.++The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>.new file mode 100644@@ -0,0 +1,25 @@+----------------------------+Supplement – svg font editor+----------------------------++Author: Johan Mattsson+License: GNU GPL v3++-------------------------------+Building Supplement from source+-------------------------------++Install vala and all required libraries, they are most likely in packages with a +-dev or -devel affix. You can run this as root if you are using Debian:++apt-get install valac libglib-2.0-dev libgtk+2.0-dev libxml2-dev++Configure, build and install with waf:+./waf configure+./waf build+su+./waf install++You need to install a sensible browser with support for svg-fonts like +chromium or epiphany if you want to view your font.+new file mode 100644Binary files /dev/null and b/icons/add_background_image.png differnew file mode 100644Binary files /dev/null and b/icons/add_new_grid.png differnew file mode 100644Binary files /dev/null and b/icons/available_characters.png differnew file mode 100644Binary files /dev/null and b/icons/background_help_lines.png differnew file mode 100644Binary files /dev/null and b/icons/cancel_save.png differnew file mode 100644Binary files /dev/null and b/icons/close_paths.png differnew file mode 100644Binary files /dev/null and b/icons/copy_path.png differnew file mode 100644Binary files /dev/null and b/icons/corner.png differnew file mode 100644Binary files /dev/null and b/icons/custom_character_set.png differnew file mode 100644Binary files /dev/null and b/icons/cut.png differnew file mode 100644Binary files /dev/null and b/icons/cut_background.png differnew file mode 100644Binary files /dev/null and b/icons/delete_grid.png differnew file mode 100644Binary files /dev/null and b/icons/delete_selected_glyph.png differnew file mode 100644Binary files /dev/null and b/icons/edit_grid.png differnew file mode 100644Binary files /dev/null and b/icons/erase_tool.png differnew file mode 100644Binary files /dev/null and b/icons/export.png differnew file mode 100644Binary files /dev/null and b/icons/file_selection.png differnew file mode 100644Binary files /dev/null and b/icons/full_glyph.png differnew file mode 100644Binary files /dev/null and b/icons/help_lines.png differnew file mode 100644Binary files /dev/null and b/icons/insert_background.png differnew file mode 100644Binary files /dev/null and b/icons/kerning_context.png differnew file mode 100644Binary files /dev/null and b/icons/move.png differnew file mode 100644Binary files /dev/null and b/icons/move_background.png differnew file mode 100644Binary files /dev/null and b/icons/move_handle.png differnew file mode 100644Binary files /dev/null and b/icons/move_handle_active.png differnew file mode 100644Binary files /dev/null and b/icons/move_layer.png differnew file mode 100644Binary files /dev/null and b/icons/move_point.png differnew file mode 100644Binary files /dev/null and b/icons/new_file.png differnew file mode 100644Binary files /dev/null and b/icons/new_grid.png differnew file mode 100644Binary files /dev/null and b/icons/new_point.png differnew file mode 100644Binary files /dev/null and b/icons/new_point_on_path.png differnew file mode 100644Binary files /dev/null and b/icons/next.png differnew file mode 100644Binary files /dev/null and b/icons/no_icon.png differnew file mode 100644Binary files /dev/null and b/icons/open.old.png differnew file mode 100644Binary files /dev/null and b/icons/open.png differnew file mode 100644Binary files /dev/null and b/icons/paste_path.png differnew file mode 100644Binary files /dev/null and b/icons/path2826.png differnew file mode 100644Binary files /dev/null and b/icons/path2863.png differnew file mode 100644Binary files /dev/null and b/icons/pen_tool.png differnew file mode 100644Binary files /dev/null and b/icons/prev.png differnew file mode 100644Binary files /dev/null and b/icons/rect4228.png differnew file mode 100644Binary files /dev/null and b/icons/resize.png differnew file mode 100644Binary files /dev/null and b/icons/reverse_path.png differnew file mode 100644Binary files /dev/null and b/icons/rotate_icon.png differnew file mode 100644Binary files /dev/null and b/icons/rotate_icon_active.png differnew file mode 100644Binary files /dev/null and b/icons/save.png differnew file mode 100644Binary files /dev/null and b/icons/show_background.png differnew file mode 100644Binary files /dev/null and b/icons/show_xheight_helplines.png differnew file mode 100644Binary files /dev/null and b/icons/show_xheight_helplines_ (kopia).png differnew file mode 100644Binary files /dev/null and b/icons/shrink.png differnew file mode 100644Binary files /dev/null and b/icons/shrink_tool.png differnew file mode 100644Binary files /dev/null and b/icons/size_to_boundries.png differnew file mode 100644Binary files /dev/null and b/icons/slow_test.png differnew file mode 100644Binary files /dev/null and b/icons/spin_button.png differnew file mode 100644Binary files /dev/null and b/icons/test_case.png differnew file mode 100644Binary files /dev/null and b/icons/test_icon.png differnew file mode 100644Binary files /dev/null and b/icons/tie_point.png differnew file mode 100644Binary files /dev/null and b/icons/tie_x_or_y.png differnew file mode 100644Binary files /dev/null and b/icons/union_paths.png differnew file mode 100644Binary files /dev/null and b/icons/utf_8.png differnew file mode 100644Binary files /dev/null and b/icons/version.png differnew file mode 100644Binary files /dev/null and b/icons/view_result.png differnew file mode 100644Binary files /dev/null and b/icons/zoom_1_1.png differnew file mode 100644Binary files /dev/null and b/icons/zoom_background_image.png differnew file mode 100644Binary files /dev/null and b/icons/zoom_boundries.png differnew file mode 100644Binary files /dev/null and b/icons/zoom_in.png differnew file mode 100644Binary files /dev/null and b/icons/zoom_out.png differnew file mode 100644Binary files /dev/null and b/icons/zoom_tool.png differnew file mode 100644@@ -0,0 +1,331 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;+using Gdk;+using Cairo;++namespace Supplement {++class BackgroundSelection : FontDisplay {++ int active_box = -1;+ ImageSurface add_icon; + Glyph glyph;+ CutBackgroundCanvas cut_background_canvas;+ List <string> background_images = new List <string> ();+ Allocation allocation;+ + public BackgroundSelection (Glyph glyph) {+ ImageSurface? i;+ i = Icons.get_icon ("add_background_image.png");+ return_if_fail (i != null);+ add_icon = (!) i;+ this.glyph = glyph;+ + CutBackgroundTool cb = (CutBackgroundTool) MainWindow.get_toolbox ().get_tool ("cut_background");+ + cb.set_destination_file_name (glyph.get_name ());+ + cb.new_image.connect ((file) => {+ Tool zoom_background;+ TabBar tb = MainWindow.get_tab_bar ();+ + glyph.set_background_image (new GlyphBackgroundImage (file));++ tb.close_display (cut_background_canvas);+ tb.select_tab_name (glyph.get_name ());+ + glyph.set_background_visible (true);+ + zoom_background = MainWindow.get_tool ("zoom_background_image");+ zoom_background.select_action (zoom_background);++ });+ }++ public override string get_name () {+ return "Backgrounds";+ }++ public override void draw (Allocation allocation, Context cr) { + double x, y, zoom;+ int box_index = 0;+ Font font;++ this.allocation = allocation;++ // bg color+ cr.save ();+ cr.set_source_rgba (255/255.0, 255/255.0, 255/255.0, 1);+ cr.rectangle (0, 0, allocation.width, allocation.height);+ cr.fill ();+ cr.restore ();+ + draw_box (box_index, 20, 20, cr);+ cr.set_source_surface (add_icon, 52, 52);+ cr.paint ();+ + cr.save ();+ x = 140;+ y = 20;+ zoom = 0.04;+ + font = Supplement.get_current_font ();+ foreach (string file in background_images) {+ box_index++;++ if (x > allocation.width - 120) {+ x = 20;+ y += 120;+ }++ draw_box (box_index, x, y, cr);+ draw_thumbnail (file, x, y, cr, box_index);+ + x += 120; + }+ cr.restore ();+ + }+ + private bool draw_thumbnail (string file, double x, double y, Context cr, int box_index) {+ ImageSurface img;+ GlyphBackgroundImage bg = new GlyphBackgroundImage (file);+ File thumbnail = bg.get_thumbnail_file ();+ File full = File.new_for_path (file);++ img = new ImageSurface.from_png ((!) thumbnail.get_path ());+ + if (img.status () != Cairo.Status.SUCCESS) {+ return false;+ }++ cr.set_source_surface (img, x + 10, y + 10);+ cr.paint ();+ + return true; + }+ + private void draw_box (int index, double x, double y, Context cr) {+ cr.save ();+ cr.set_line_join (LineJoin.ROUND);+ cr.set_line_width (7);+ + if (index == active_box) {+ cr.set_source_rgba (203/255.0, 220/255.0, 249/255.0, 1);+ } else {+ cr.set_source_rgba (183/255.0, 200/255.0, 223/255.0, 1);+ }+ + cr.rectangle (x, y, 100, 100);+ cr.fill_preserve ();+ cr.stroke ();+ + cr.restore (); + }+ + public override void button_release (EventButton e) {+ motion (e.x, e.y);+ + if (active_box == 0) {+ add_image ();+ } else {+ select_image ();+ }+ }++ private void add_image () {+ FileChooserDialog file_chooser;+ string file;+ ImageSurface image;+ + try {+ file_chooser = new FileChooserDialog ("Add background image", MainWindow.get_current_window (), FileChooserAction.OPEN, Stock.CANCEL, ResponseType.CANCEL, Stock.OPEN, ResponseType.ACCEPT);+ file_chooser.set_current_folder_file (Supplement.get_current_font ().get_folder ());+ + if (file_chooser.run () == ResponseType.ACCEPT) {+ // Fixa: Set file name relative to font folder+ file = file_chooser.get_filename ();++ image = new ImageSurface.from_png (file);+ + if (image.status () == Cairo.Status.SUCCESS) {+ Supplement.get_current_font ().add_background_image (file);+ MainWindow.get_glyph_canvas ().redraw (); + }+ }++ file_chooser.destroy ();+ } catch (GLib.Error e) {+ warning (e.message);+ }+ + collect_background_images ();+ }++ private void select_image () {+ Font font;+ string file;+ GlyphBackgroundImage bg;+ TabBar tb;+ Tool zoom_background;++ font = Supplement.get_current_font ();+ + if (!(0 <= active_box - 1 < background_images.length ())) {+ return;+ }+ + file = background_images.nth (active_box - 1).data; // Fixa: check bounds+ bg = new GlyphBackgroundImage (file);+ tb = MainWindow.get_tab_bar ();+ cut_background_canvas = new CutBackgroundCanvas (glyph);+ + bg.reset_scale (glyph);+ + cut_background_canvas.set_background_image (bg);+ + tb.select_tab_name (glyph.get_name ());+ tb.close_display (this);+ + tb.add_tab (cut_background_canvas, 110, false);++ cut_background_canvas.set_show_help_lines (false);+ cut_background_canvas.set_background_visible (true);++ zoom_background = MainWindow.get_tool ("zoom_background_image");+ zoom_background.select_action (zoom_background);++ MainWindow.get_toolbox ().select_tool_by_name ("cut_background");++ }++ private void add_unique_background (string file) {+ foreach (var f in background_images) {+ if (f == file) {+ return;+ }+ }+ + background_images.prepend (file);+ }++ public override void selected_canvas () {+ Font font = Supplement.get_current_font ();+ File folder = font.get_backgrounds_folder ();+ + if (!folder.query_exists ()) {+ DirUtils.create ((!) folder.get_path (), 0xFFFFFF);+ }+ + collect_background_images ();+ }++ bool is_image (string file_name) {+ return file_name.index_of (".png") != -1 + || file_name.index_of (".jpg") != -1+ || file_name.index_of (".jpeg") != -1;+ }++ void collect_background_images () {+ Font font = Supplement.get_current_font ();+ File directory = font.get_folder ();+ FileEnumerator enumerator;+ FileInfo? file_info;+ FileInfo fi;+ File img_file;++ while (background_images.length () > 0) {+ background_images.delete_link (background_images.first ());+ }+ + foreach (string file in font.background_images) {+ add_unique_background (file);+ }+ + try {+ enumerator = directory.enumerate_children (FILE_ATTRIBUTE_STANDARD_NAME, 0);+ + while ((file_info = enumerator.next_file ()) != null) {+ fi = (!) file_info;+ + if (is_image (fi.get_name ())) {+ img_file = directory.get_child (fi.get_name ()); + add_unique_background ((!)img_file.get_path ());+ }+ + }+ } catch (GLib.Error e) {+ warning (e.message);+ }+ }+ + public override void key_press (EventKey e) {+ }++ public override void key_release (EventKey e) {+ }+ + public override void motion_notify (EventMotion e) {+ motion (e.x, e.y);+ }+ + public void motion (double x, double y) {+ int active;+ + active = (int) (x / 120); // column+ active += (int) (y / 120) * (int) (allocation.width / 120.0); // row+ + if (active != active_box) {+ MainWindow.get_glyph_canvas ().redraw ();+ active_box = active;+ }+ }++ public override void button_press (EventButton e) {+ motion (e.x, e.y);+ }++ public override void store_current_view () {+ }+ + public override void restore_last_view () {+ }++ public override void next_view () {+ }+ + public override void scroll_wheel_up (Gdk.EventScroll e) {+ }++ public override void scroll_wheel_down (Gdk.EventScroll e) {+ }++}++class CutBackgroundCanvas : Glyph {+ Glyph glyph;++ public CutBackgroundCanvas (Glyph g) {+ base ("Background");+ glyph = g;+ redraw_help_lines ();+ }+}++}new file mode 100644@@ -0,0 +1,157 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;+using Gdk;++namespace Supplement {++class BackgroundTool : Tool {++ double begin_x = 0;+ double begin_y = 0;++ public double img_offset_x = 0;+ public double img_offset_y = 0;++ public double img_width = 0;+ public double img_height = 0;+ + public double img_scale_x = 0;++ public static double top_limit;+ public static double bottom_limit;+ + bool move_bg;+ bool resize_bg; // DEL+ + public BackgroundTool (string name) {+ base (name, "move, resize and rotate background image", 'M', CTRL|SHIFT);+ + top_limit = 0;+ bottom_limit = 0;++ select_action.connect((self) => {+ });++ deselect_action.connect ((self) => {+ });+ + press_action.connect((self, b, x, y) => {+ Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? bg = g.get_background_image ();++ bool selection = MainWindow.get_current_display () is CutBackgroundCanvas; ++ if (!selection) {+ if (bg == null) return;+ ((!)bg).handler_press (x, y);+ }+ + begin_x = x;+ begin_y = y;+ + img_offset_x = ((!)bg).img_offset_x;+ img_offset_y = ((!)bg).img_offset_y;+ + img_scale_x = ((!)bg).img_scale_x;+ + img_width = ((!)bg).get_img ().get_width () * img_scale_x;+ img_height = ((!)bg).get_img ().get_height () * ((!)bg).img_scale_y;+ + move_bg = true;+ });++ release_action.connect((self, b, x, y) => {+ Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? bg = g.get_background_image ();+ + if (bg == null) return;+ + img_offset_x = ((!)bg).img_offset_x;+ img_offset_y = ((!)bg).img_offset_y;+ + ((!)bg).handler_release (x, y);+ + move_bg = false;+ resize_bg = false;+ });++ move_action.connect((self, x, y) => { + move (x, y);+ });+ + draw_action.connect ((self, cairo_context, glyph) => {+ bool selection;+ Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? background_image = g.get_background_image ();+ if (background_image == null) return;+ + selection = MainWindow.get_current_display () is CutBackgroundCanvas;+ + if (!selection) {+ ((!) background_image).draw_handle (cairo_context, glyph);+ }+ });+ }+ + void move (double x, double y) {+ Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? background_image = g.get_background_image ();+ GlyphBackgroundImage bg = (!) background_image;+ + double xscale, yscale, dx, dy;++ if (background_image == null) return;+ + bg.handler_move (x, y);+ + if (!(move_bg || resize_bg)) return;+ + dx = x - begin_x;+ dy = y - begin_y;+ + dx *= 1/g.view_zoom;+ dy *= 1/g.view_zoom;+ + if (bg.selected_handle == 2) {+ bg.set_img_rotation_from_coordinate (x, y);+ }+ + if (bg.selected_handle == 1) {+ xscale = img_scale_x * ((img_width - dx) / img_width); + yscale = xscale;++ bg.set_img_scale (xscale, yscale);+ bg.set_img_offset (this.img_offset_x + dx, this.img_offset_y);+ } + + if (move_bg && bg.selected_handle <= 0) {+ bg.set_img_offset (this.img_offset_x + dx, this.img_offset_y + dy);+ }++ MainWindow.get_glyph_canvas ().redraw ();+ }+ + public override bool test () {+ return false;+ }++}++}+new file mode 100644@@ -0,0 +1,62 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class CharDatabase {++ public CharDatabase () {+ }++ public static bool has_ascender (unichar c) {+ if (!c.islower()) return true;+ + // todo: their derrivatives+ switch (c) {+ case 'b': return true;+ case 'd': return true;+ case 'f': return true;+ case 'h': return true;+ case 'i': return true;+ case 'j': return true;+ case 'k': return true;+ case 'l': return true; + }+ + if ('à' <= c <= 'å') return true;+ if ('è' <= c <= 'ö') return true;+ if ('ù' <= c <= 'ă') return true;+ if ('ć' <= c <= 'ė') return true;++ return false;+ }++ public static bool has_descender (unichar c) {+ // todo: their derrivatives+ switch (c) {+ case 'g': return true;+ case 'j': return true;+ case 'p': return true;+ case 'q': return true;+ }+ + return false; + }+ +}++}new file mode 100644@@ -0,0 +1,94 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class ClipTool : Tool {++ static Glyph? glyph = null; + static Path? path = null; ++ public static void copy () {+ Font font = Supplement.get_current_font ();+ OverView overview = MainWindow.get_overview ();+ FontDisplay fd = MainWindow.get_current_display ();+ string gc;+ + if (fd is OverView) {+ gc = overview.get_selected_char ();+ + print (@"SELECTED: $gc \n");+ + glyph = font.get_glyph (gc);+ path = null;+ }++ if (fd is Glyph) {+ glyph = null;+ path = MainWindow.get_current_glyph ().active_path;+ + if (path == null) {+ glyph = MainWindow.get_current_glyph ();+ }+ }++ }++ public static void paste () {+ Font font = Supplement.get_current_font ();+ OverView overview = MainWindow.get_overview ();+ FontDisplay fd = MainWindow.get_current_display ();+ string gc;+ Glyph? destination = null;+ unichar new_char;+ + if (fd is OverView) {+ gc = overview.get_selected_char ();+ destination = font.get_glyph (gc);+ + if (destination == null) {+ new_char = gc.get_char (0);+ destination = new Glyph (gc, new_char) ;+ font.add_glyph ((!) destination);+ }+ }+ + if (fd is Glyph) {+ destination = (Glyph) fd;+ }+ + return_if_fail (destination != null);++ if (destination == glyph) {+ return;+ }+ + if (glyph != null) {+ foreach (Path p in ((!)glyph).path_list) {+ ((!)destination).add_path (p.copy ());+ }+ }++ if (path != null) {+ ((!)destination).add_path (((!)path).copy ());+ }+ + }++}++}new file mode 100644@@ -0,0 +1,606 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;+using Gdk;+using Cairo;++namespace Supplement {++class ContentDisplay : FontDisplay {++ List<Font> recent_fonts = new List<Font> ();++ int recent_active = -1;+ int open_active = -1;++ const int NEW_FONT = 0;+ const int LOAD_FONT = 1;+ const int SAVE_FONT = 2;++ Allocation allocation;+ int num_collumns = 0;++ Tool new_file_tool;+ Tool open_tool;+ Tool save_tool;+ Tool browser_tool;+ + ExportTool export_tool;++ Tool kerning_context_tool;++ Tool add_new_grid_tool;+ Tool delete_grid_tool;+ + public List<Tool> tools;+ + StringBuilder name;+ int has_modifier = 0;+ bool marker = true;+ bool show_marker = true;+ TimeoutSource marker_blink;+ + public ContentDisplay () {+ new_file_tool = new Tool ("new_file", "New file", 'n', CTRL);+ new_file_tool.x = 15;+ new_file_tool.y = 25;+ new_file_tool.select_action.connect((self) => {+ new_file ();+ });+ + open_tool = new Tool ("open", "Open", 'o', CTRL);+ open_tool.x = 50;+ open_tool.y = 25;+ open_tool.select_action.connect((self) => {+ load ();+ });++ save_tool = new Tool ("save", "Save", 's', CTRL);+ save_tool.x = 85;+ save_tool.y = 25;+ save_tool.select_action.connect((self) => {+ save ();+ });+ + browser_tool = new Tool ("view_result", "Export font and view result in web browser", ',', CTRL);+ browser_tool.x = 120;+ browser_tool.y = 25;+ browser_tool.select_action.connect((self) => {+ export_to_browser ();+ });++ export_tool = new ExportTool ("export");+ export_tool.x = 155;+ export_tool.y = 25;+ + kerning_context_tool = new Tool ("kerning_context", "Show kerning context", 'k', CTRL);+ kerning_context_tool.x = 200;+ kerning_context_tool.y = 25;+ kerning_context_tool.select_action.connect((self) => {+ MainWindow.get_tab_bar ().add_unique_tab (new ContextDisplay (), 65, false);+ });+ + add_new_grid_tool = new Tool ("add_new_grid", "Add new grid");+ add_new_grid_tool.x = 15;+ add_new_grid_tool.y = 95;+ add_new_grid_tool.select_action.connect((self) => {+ MainWindow.get_toolbox ().add_new_grid ();+ });+ + delete_grid_tool = new Tool ("delete_grid", "Remove grid");+ delete_grid_tool.x = 50;+ delete_grid_tool.y = 95;+ delete_grid_tool.select_action.connect((self) => {+ MainWindow.get_toolbox ().remove_current_grid ();+ });+ + tools = new List<Tool> ();+ tools.append (new_file_tool);+ tools.append (open_tool);+ tools.append (save_tool);+ tools.append (browser_tool);+ tools.append (kerning_context_tool);+ tools.append (add_new_grid_tool);+ tools.append (delete_grid_tool);+ tools.append (export_tool);+ + name = new StringBuilder ();+ name.append (Supplement.get_current_font ().get_name ());+ + marker = true;+ marker_blink = new TimeoutSource (1200);+ marker_blink.set_callback (() => {+ show_marker = !show_marker;+ MainWindow.get_glyph_canvas ().redraw (); + return marker;+ });+ marker_blink.attach (null);+ + propagate_recent_files ();+ + foreach (Tool t in tools) {+ t.set_default_color (1, 1, 1);+ }+ }++ private void save () {+ SaveDialog s = new SaveDialog ();+ save_dialog (s);+ }++ private void save_dialog (SaveDialog s) {+ FontDisplay fd = MainWindow.get_current_display ();+ + // display save tab or save font if save dialog is open + if (fd is SaveDialog) {+ s = ((SaveDialog) fd);+ s.save.select_action (s.save);+ } else if (Supplement.get_current_font ().is_modified ()) {+ MainWindow.get_tab_bar ().add_unique_tab (s, 50);+ } else {+ s.finished ();+ }+ }++ private void new_file () {+ SaveDialog save = new SaveDialog ();+ save.finished.connect (() => {+ Font f = Supplement.get_current_font ();+ f.delete_backup ();+ + Supplement.new_font ();+ MainWindow.close_all_tabs ();+ + select_overview ();+ });+ save_dialog (save); + }+ + private void load () {+ SaveDialog save = new SaveDialog ();+ save.finished.connect (() => {+ load_new_font ();+ });+ save_dialog (save);+ }+ + private void export_to_browser () {+ export_tool.view_result ();+ }+ + public override string get_name () {+ return "Content";+ }++ public override void draw (Allocation allocation, Context cr) { + this.allocation = allocation;+ + if (num_collumns == 0) {+ propagate_recent_files ();+ }+ + // bg color+ cr.save ();+ cr.set_source_rgba (1, 1, 1, 1);+ cr.rectangle (0, 0, allocation.width, allocation.height);+ cr.fill ();+ cr.restore ();+ + // draw recent fonts+ draw_recent_fonts (allocation, cr);+ + // new and open+ draw_menu (allocation, cr);+ + }+ + public override void selected_canvas () {+ KeyBindings.singleton.set_require_modifier (true);+ + name.erase ();+ name.append (Supplement.get_current_font ().get_name ());+ + propagate_recent_files ();+ }++ public void propagate_recent_files () {+ Font font;+ this.num_collumns = (int) (allocation.width / 150.0);+ + while (recent_fonts.length () != 0) {+ recent_fonts.delete_link (recent_fonts.first ());+ }+ + foreach (var f in Preferences.get_recent_files ()) {+ if (f == "") continue;+ + File file = File.new_for_path (f);++ font = new Font ();+ + font.set_font_file (f);+ + if (file.query_exists ()) { + recent_fonts.append (font);+ }+ }+ + recent_fonts.reverse ();+ }++ private void draw_recent_fonts (Allocation allocation, Context cr) {+ double x = 10;+ double y = 260 + 25;+ int i = 0;+ int columns = (int) (allocation.width / 150.0);+ string fn;+ + if (recent_fonts.length () == 0) {+ return;+ }+ + cr.save ();++ cr.set_font_size (14);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + cr.move_to (x, y);+ cr.show_text ("Recent fonts");++ y = 260 + 45;+ x = 10;+ + foreach (var font in recent_fonts) {+ string svg = font.get_svg_thumbnail ();+ string font_file = (!) font.font_file;+ + cr.save ();+ + if (recent_active == i) + cr.set_source_rgba (84/255.0, 134/255.0, 148/255.0, 1);+ else+ cr.set_source_rgba (176/255.0, 211/255.0, 230/255.0, 1);+ + cr.set_line_join (LineJoin.ROUND);+ cr.set_line_width (7);+ + cr.rectangle (x, y, 130, 130);+ cr.fill_preserve ();+ cr.stroke ();+ cr.restore ();+ + Svg.draw_svg_path (cr, svg, x + 40, y + 90);+ + cr.save ();+ cr.move_to (x + 3, y + 155);+ cr.set_font_size (10);+ cr.set_source_rgba(0, 0, 0, 1);+ + font_file = (!) font.font_file;+ + fn = font_file.substring (font_file.replace ("\\", "/").last_index_of ("/") + 1);+ + if (fn.char_count () > 17) {+ fn = fn.substring (0, 17) + " ...";+ }+ + cr.show_text (fn);+ cr.restore ();+ + x += 150;+ i++;+ + if (--columns <= 0) {+ break;+ }+ }+ + cr.stroke ();+ cr.restore ();+ + }++ private void draw_menu (Allocation allocation, Context cr) {+ draw_file_menu (allocation, cr);+ draw_grid_menu (allocation, cr);+ draw_font_name (allocation, cr);+ }+ + private void draw_file_menu (Allocation allocation, Context cr) {+ double x;+ double y; + + x = 10;+ y = 25;++ cr.save ();+ + cr.move_to (x, y);+ + cr.set_font_size (14);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + cr.move_to (x, y);+ cr.show_text ("File");+ + cr.move_to (200, y);+ cr.show_text ("Views");+ + cr.restore ();++ save_tool.draw (allocation, cr);+ new_file_tool.draw (allocation, cr);+ open_tool.draw (allocation, cr);+ browser_tool.draw (allocation, cr);+ export_tool.draw (allocation, cr);+ + kerning_context_tool.draw (allocation, cr);+ }+ + private void draw_grid_menu (Allocation allocation, Context cr) {+ double x;+ double y; + + x = 10;+ y = 95;++ cr.save ();+ + cr.set_font_size (14);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + cr.move_to (x, y);+ cr.show_text ("Grid width");++ cr.move_to (x + 100, y);+ cr.show_text (""); // Grid angle+ + cr.restore ();+ + add_new_grid_tool.draw (allocation, cr);+ delete_grid_tool.draw (allocation, cr);+ }+ + private void draw_font_name (Allocation allocation, Context cr) {+ double x;+ double y; + string m;+ + x = 10;+ y = 165;++ cr.save ();+ + cr.set_font_size (14);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + cr.move_to (x, y);+ cr.show_text ("Name");++ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.NORMAL);+ cr.set_font_size (16);+ cr.move_to (x + 1, y + 20);+ + m = (show_marker) ? "|" : "";+ + cr.show_text (name.str + m);+ + cr.restore ();+ + }+ + public override void key_press (EventKey e) {+ unichar c = (unichar) e.keyval;+ Font f;+ + if (e.type != EventType.KEY_PRESS) { + return;+ }+ + if (is_mod (e)) {+ return;+ }+ + if (KeyBindings.singleton.modifier != NONE && KeyBindings.singleton.modifier != SHIFT) {+ return;+ }+ + if (e.keyval == Key.BACK_SPACE) { + name.truncate (name.len - 1);+ + while (!name.str.validate ()) {+ name.truncate (name.len - 1);+ + if (name.str.length == 0) {+ break;+ }+ }+ }+ + if (has_modifier == 0 && !is_modifier_key (e.keyval) && c.validate ()) {+ + // webkit does not allow full unicode characters or space in name+ // replace non-ansi characters with _++ if ('0' <= c <= '9' || 'a' <= c <= 'z' || 'A' <= c <= 'Z') {+ name.append_unichar (c);+ } else {+ name.append_unichar ('_');+ }+ + f = Supplement.get_current_font ();+ f.set_name (name.str);+ f.touch ();+ }++ MainWindow.get_glyph_canvas ().redraw (); + }++ public override void key_release (EventKey e) {+ if (is_mod (e)) {+ has_modifier--;+ }+ }+ + public override void motion_notify (EventMotion e) {+ int column = (int)(e.x / 160);+ bool a;+ + if (280 < e.y < 280 + 150) {+ recent_active = column;+ } else {+ recent_active = -1;+ }++ if (44 < e.y < 181) {+ open_active = column;+ } else {+ open_active = -1;+ }++ foreach (Tool t in tools) {+ a = t.is_over (e.x, e.y);+ t.set_active (a);+ + if (a) {+ MainWindow.get_tool_tip ().set_text_from_tool ();+ }+ }++ MainWindow.get_glyph_canvas ().redraw ();+ }++ public override void button_release (EventButton event) {+ int columns = (int) (allocation.width / 150.0) - 1;+ SaveDialog save = new SaveDialog ();+ Font cf = Supplement.get_current_font ();+ string f, f2;+ + foreach (Tool t in tools) {+ if (t.is_over (event.x, event.y)) {+ activate (t);+ }+ }++ if (0 <= recent_active <= columns) {+ f = (!) cf.font_file;+ f2 = (!) recent_fonts.nth (recent_active).data.font_file; ++ if (cf.font_file != null && f == f2) { // recent font is already loaded+ MainWindow.get_toolbox ().select_tool_by_name ("available_characters");+ } else { + save.finished.connect (() => {+ Supplement.get_current_font ().delete_backup ();+ load_recent_font ();+ });+ save_dialog (save);+ }+ }+ }+ + private void activate (Tool t) {+ t.select_action (t);+ MainWindow.get_tool_tip ().set_text_from_tool ();+ }+ + private void load_new_font () {+ string? fn;+ FileChooserDialog file_chooser = new FileChooserDialog ("Open font file", MainWindow.get_current_window (), FileChooserAction.OPEN, Stock.CANCEL, ResponseType.CANCEL, Stock.OPEN, ResponseType.ACCEPT);+ Font f = Supplement.get_current_font ();+ + file_chooser.set_current_folder_file (f.get_folder ());+ + if (file_chooser.run () == ResponseType.ACCEPT) { + MainWindow.get_glyph_canvas ().redraw ();+ + fn = file_chooser.get_filename ();++ if (fn != null) {+ f.delete_backup ();+ + MainWindow.clear_glyph_cache ();+ MainWindow.close_all_tabs ();+ f.load ((!)fn);+ + select_overview (); + Preferences.add_recent_files ((!) fn);+ }+ }+ + file_chooser.destroy ();+ }++ private void select_overview () {+ Toolbox tb = MainWindow.get_toolbox ();+ + if (Supplement.get_current_font ().is_empty ()) {+ tb.select_tool_by_name ("custom_character_set");+ } else {+ tb.select_tool_by_name ("available_characters"); + }+ }++ private void load_recent_font () {+ string? fnn;+ string fn;+ Font f;+ bool loaded;+ + if (0 <= recent_active < recent_fonts.length ()) {+ fnn = recent_fonts.nth (recent_active).data.font_file;++ return_if_fail (fnn != null);+ + fn = (!) fnn; ++ f = Supplement.get_current_font ();+ f.delete_backup ();+ + MainWindow.clear_glyph_cache ();+ MainWindow.close_all_tabs ();+ + loaded = f.load (fn);+ + if (!unlikely (loaded)) {+ warning (@"Failed to load fond $fn");+ } else {+ Preferences.add_recent_files (fn); + select_overview ();+ }+ }+ }++ public override void button_press (EventButton event) {+ bool a;+ foreach (Tool t in tools) {+ a = t.is_over (event.x, event.y);+ t.set_active (a);+ + if (a) {+ MainWindow.get_tool_tip ().set_text_from_tool ();+ }+ }+ + if (167 < event.y < 193) {+ + }+ }++}++}new file mode 100644@@ -0,0 +1,317 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;+using Gdk;+using Cairo;++namespace Supplement {++/** Kerning context. */+class ContextDisplay : FontDisplay {++ List <Word> row;+ int active_handle = -1;+ int selected_handle = -1;+ + double begin_handle_x = 0;+ double begin_handle_y = 0;+ + double current_kerning = 0;+ + public ContextDisplay () {+ Word w = new Word ();+ row = new List <Word> ();+ row.append (w);+ }++ public override string get_name () {+ return "Context";+ }+ + public override void draw (Allocation allocation, Context cr) {+ Glyph? g;+ Glyph glyph;+ double x, y, w, kern, alpha;+ double x2;+ int i, wi;+ string prev;+ + i = 0;+ + // bg color+ cr.save ();+ cr.set_source_rgba (1, 1, 1, 1);+ cr.rectangle (0, 0, allocation.width, allocation.height);+ cr.fill ();+ cr.restore ();+ + alpha = 1;+ y = 100;+ x = 20;+ w = 0;+ prev = "";+ + foreach (Word word in row) {+ wi = 0;+ + foreach (string s in word.glyph) { + g = (!) Supplement.get_current_font ().get_glyph (s); + kern = get_kerning (prev, s);+ + if (s == "") {+ continue;+ }+ + // draw glyph + if (g == null) {+ w = 50;+ alpha = 1;+ } else {+ alpha = 0;+ glyph = (!) g;+ Svg.draw_svg_path (cr, glyph.get_svg_data (), x + kern, y);+ w = glyph.get_width ();+ }++ // handle+ if (active_handle == i && active_handle < word.glyph.length ()) {+ x2 = x + kern;+ + cr.save ();+ cr.set_source_rgba (153/255.0, 153/255.0, 173/255.0, 1);+ cr.move_to (x2 - 5, y + 20);+ cr.line_to (x2 + 0, y + 20 - 5);+ cr.line_to (x2 + 5, y + 20);+ cr.fill ();+ + cr.set_font_size (10);+ cr.show_text (s);+ cr.restore ();+ } + + x += w + kern;+ + // caption+ if (g == null || ((!)g).is_empty ()) {+ cr.save ();+ cr.set_source_rgba (153/255.0, 153/255.0, 153/255.0, alpha);+ cr.move_to (x - w / 2.0 - 5, y + 20);+ cr.set_font_size (10);+ cr.show_text (s);+ cr.restore ();+ }+ + prev = s;+ + wi++;+ i++;+ }+ + if (y > allocation.height && i > 10) {+ row.remove (word);+ }+ + y += MainWindow.get_current_glyph ().get_height () + 20;+ x = 20; + }+ }+ + private void set_kerning (int handle, double val) {+ string a, b;+ Font font;+ int wi = 0;+ double kern;+ + font = Supplement.get_current_font ();++ font.touch ();++ handle++; // use handle before next glyph++ a = "";+ b = "";++ foreach (Word word in row) {+ foreach (string s in word.glyph) {+ b = s;+ + if (handle == wi) {+ kern = current_kerning + val;+ font.set_kerning (a, b, kern);+ }+ + wi++;+ + a = b;+ }+ }+ + + }+ + private double get_kerning (string a, string b) {+ Font font = Supplement.get_current_font ();+ return font.get_kerning (a, b);+ }+ + public override void selected_canvas () {+ Glyph g;+ Word w;+ unowned List<Word>? r;+ unowned List<string>? rw;+ StringBuilder s = new StringBuilder ();+ bool append_char = false;+ + KeyBindings.singleton.set_require_modifier (true);+ + g = MainWindow.get_current_glyph ();+ s.append_unichar (g.get_unichar ());++ r = row;+ return_if_fail (r != null);++ if (row.length () == 0) {+ append_char = true;+ }+ + if (row.length () > 0) {+ rw = row.first ().data.glyph;+ return_if_fail (rw != null);+ append_char = (row.first ().data.glyph.data.index_of (s.str) == -1);+ }+ + if (append_char) {+ w = new Word ();+ row.append (w);+ w.glyph.prepend (s.str);+ } + }+ + public override void key_press (EventKey e) {+ unichar c = (unichar) e.keyval;+ StringBuilder s = new StringBuilder ();+ + if (e.type == EventType.KEY_PRESS && (KeyBindings.singleton.modifier == NONE || KeyBindings.singleton.modifier == SHIFT)) {+ + if (e.keyval == Key.BACK_SPACE && row.length () > 0) { + row.first ().data.glyph.remove_link (row.first ().data.glyph.last ());+ }+ + if (row.length () == 0 || c == Key.ENTER) {+ row.prepend (new Word ());+ }+ + if (!is_modifier_key (c) && c.validate ()) {+ s.append_unichar (c);+ row.first ().data.glyph.append (s.str);+ }+ }+ + MainWindow.get_glyph_canvas ().redraw ();+ }+ + public override void motion_notify (EventMotion e) {+ double k, y;+ + if (selected_handle == -1) {+ set_active_handle (e.x, e.y);+ } else {+ y = 1;+ + if (Math.fabs (e.y - begin_handle_y) > 20) {+ y = ((Math.fabs (e.y - begin_handle_y) / 100) + 1);+ }+ + k = (e.x - begin_handle_x) / y; // y-axis is variable presition // Fixa: spell+ set_kerning (selected_handle, k);+ MainWindow.get_glyph_canvas ().redraw ();+ }+ }+ + public void set_active_handle (double ex, double ey) {+ double y = 100;+ double x = 20;+ double w = 0;+ double d, kern;+ double min = double.MAX;+ int i = 0;+ Glyph? g;+ Glyph glyph;+ + string prev = "";+ + foreach (Word word in row) {+ foreach (string s in word.glyph) {+ // draw glyph+ g = (!) Supplement.get_current_font ().get_glyph (s); + kern = get_kerning (prev, s);+ + if (g == null) {+ w = 50; + } else {+ glyph = (!) g;+ w = glyph.get_width ();+ }+ + d = Math.pow (x + kern - ex, 2) + Math.pow (y - ey, 2);+ + if (d < min) {+ min = d;+ + if (active_handle != i) {+ active_handle = i;+ current_kerning = get_kerning (prev, s);+ MainWindow.get_glyph_canvas ().redraw ();+ }+ }+ + prev = s;+ x += w + kern;+ i++;+ }+ + y += MainWindow.get_current_glyph ().get_height () + 20;+ x = 20; + }+ }+ + public override void button_release (EventButton e) {+ set_active_handle (e.x, e.y);+ selected_handle = -1;+ }+ + public override void button_press (EventButton e) {+ set_active_handle (e.x, e.y);+ selected_handle = active_handle;+ begin_handle_x = e.x;+ begin_handle_y = e.y;+ }++}++class Word {+ public List <string> glyph;+ + public Word () {+ glyph = new List <string> ();+ glyph.append ("");+ }++}++}new file mode 100644@@ -0,0 +1,207 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {++class CutBackgroundTool : Tool {++ double x1 = 0;+ double y1 = 0;+ double x2 = 0;+ double y2 = 0;+ bool cut_background_is_visible = false;+ bool cut_background_is_moving = false;+ + public signal void new_image (string file);+ + string? destination_file_name = null;+ + public CutBackgroundTool (string name) {+ base (name, "cut background image", 'u', CTRL); ++ select_action.connect((self) => {+ });++ deselect_action.connect ((self) => {+ });+ + press_action.connect((self, b, x, y) => {+ bool selection = MainWindow.get_current_display () is CutBackgroundCanvas;++ if (!selection) {+ return;+ }+ + if (!is_over_rectangle (x, y)) {+ x1 = x;+ y1 = y;+ + cut_background_is_moving = true;+ cut_background_is_visible = true;+ } else {+ + do_cut ();+ + cut_background_is_visible = false;+ cut_background_is_moving = false;+ }+ + });++ release_action.connect((self, b, x, y) => {+ x2 = x;+ y2 = y;+ + cut_background_is_moving = false;+ });++ move_action.connect((self, x, y) => {+ if (cut_background_is_moving) {+ x2 = x;+ y2 = y;+ }+ + MainWindow.get_glyph_canvas ().redraw ();+ });+ + draw_action.connect ((self, cr, glyph) => {+ if (cut_background_is_visible) {+ cr.save ();+ cr.set_line_width (2.0);+ cr.set_source_rgba (0, 0, 1, 0.3);+ cr.rectangle (fmin (x1, x2), fmin (y1, y2), fabs (x1 - x2), fabs (y1 - y2));+ cr.fill_preserve ();+ cr.restore ();+ }+ });++ }++ public void set_destination_file_name (string fn) {+ destination_file_name = fn;+ }++ bool is_over_rectangle (double x, double y) {+ return fmin (x1, x2) < x < fmax (x1, x2) && fmin (y1, y2) < y < fmax (y1, y2);+ }++ double get_width () {+ return fabs (x1 - x2);+ }++ double get_height () {+ return fabs (y1 - y2);+ }++ // Todo: nice gimp style crop+ void do_cut () {+ double x, y;+ int h, w;+ + Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? b = g.get_background_image ();+ GlyphBackgroundImage bg = (!) b;+ + ImageSurface img;+ + Surface sr;+ Context cr;+ + if (b == null) {+ return;+ }++ img = bg.get_img ();++ w = (int) (get_width () / g.view_zoom);+ h = (int) (get_height () / g.view_zoom);++ sr = new Surface.similar (img, img.get_content (), w, h);+ cr = new Context (sr);+ + // fine for 1:1 but not for scaled image+ x = bg.img_offset_x;+ y = bg.img_offset_y;+ + x -= fmin (x1, x2);+ y -= fmin (y1, y2);++ x *= Glyph.ivz ();+ y *= Glyph.ivz ();++ x += Glyph.xc ();+ y += Glyph.yc ();++ x -= g.view_offset_x;+ y -= g.view_offset_y;++ x -= bg.img_offset_x * (Glyph.ivz () - 1);+ y -= bg.img_offset_y * (Glyph.ivz () - 1);++ cr.save ();+ cr.set_source_rgba (1, 1, 1, 1);+ cr.rectangle (0, 0, w, h);+ cr.fill ();+ cr.restore ();+ + cr.set_source_surface (img, x, y);+ cr.paint ();+ + save_img (sr, g);+ + cr.restore (); + }+ + void save_img (Surface sr, Glyph g) {+ GlyphBackgroundImage newbg;+ Font f = Supplement.get_current_font ();+ File img_dir;+ File img_file;+ string fn;+ string name;+ + if (destination_file_name != null) {+ name = (!) destination_file_name;+ } else {+ name = g.get_name ();+ }+ + img_dir = f.get_backgrounds_folder ().get_child ("parts");++ if (!img_dir.query_exists ()) {+ DirUtils.create ((!) img_dir.get_path (), 0xFFFFFF);+ }+ + img_file = img_dir.get_child (@"$(name).png");+ fn = (!) img_file.get_path ();+ + sr.write_to_png (fn);+ + newbg = new GlyphBackgroundImage (fn);+ g.set_background_image (newbg);+ newbg.reset_scale (g);+ + new_image (fn);+ }+}++}new file mode 100644@@ -0,0 +1,68 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class CutTool : Tool {++ PenTool pen;+ + public CutTool (string n) {+ base (n, "Cut", 'u', CTRL);+ + select_action.connect((self) => {+ pen = (PenTool) MainWindow.get_tool ("pen_tool");+ });+ + press_action.connect((self, b, x, y) => {+ pen.press_action (pen, b, x, y);+ });++ release_action.connect((self, b, x, y) => {+ pen.release_action (pen, b, x, y);+ });+ + move_action.connect ((self, x, y) => {+ pen.move_action (pen, x, y);+ });+ }++ public static void force_direction () {+ Glyph g = MainWindow.get_current_glyph ();+ Path p;+ Tool current_tool = MainWindow.get_toolbox ().get_current_tool ();+ + if (g.active_path == null) {+ return;+ }+ + p = (!) g.active_path;+ + if (p.is_editable () && p.is_open ()) {+ + if (current_tool is PenTool) {+ p.force_direction (Direction.CLOCKWISE);+ } else if (current_tool is CutTool) {+ p.force_direction (Direction.COUNTER_CLOCKWISE);+ }+ + } + }+ +}++}new file mode 100644@@ -0,0 +1,243 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;++namespace Supplement {++enum MenuDirection {+ DROP_DOWN,+ POP_UP;+}++class DropMenu : GLib.Object {++ public delegate void Selected (MenuAction self);+ public signal void selected (DropMenu self);+ + double x = -1;+ double y = -1;+ bool menu_visible = false;++ List <MenuAction> actions = new List <MenuAction> ();++ const int item_height = 25;+ + MenuDirection direction = MenuDirection.DROP_DOWN;+ + ImageSurface? icon = null;+ + public DropMenu (string icon_file = "") {+ if (icon_file != "") {+ icon = Icons.get_icon (@"$icon_file.png");+ }+ }+ + public void deselect_all () {+ foreach (MenuAction m in actions) {+ m.set_selected (false);+ }+ }+ + public void set_direction (MenuDirection d) {+ direction = d;+ }+ + public void close () {+ menu_visible = false;+ }+ + public MenuAction add_item (string label) {+ MenuAction m = new MenuAction (label);+ add_menu_item (m);+ return m;+ }+ + public void add_menu_item (MenuAction m) {+ m.parent = this;+ actions.append (m);+ }+ + public bool is_over_icon (double px, double py) {+ if (x == -1 || y == -1) {+ return false;+ }+ + return x - 5 < px < x + 12 + 5 && y - 5 < py < y + 12 + 5;+ }++ public bool menu_item_action (double px, double py) {+ MenuAction? action;++ if (menu_visible) {+ action = get_menu_action_at (px, py);+ + if (action != null) {+ ((!) action).action ((!)action);+ selected (this);+ menu_visible = false;+ return true;+ }+ }+ + return false;+ }+ + public bool menu_icon_action (double px, double py) { + menu_visible = is_over_icon (px, py);+ return menu_visible;+ }+ + MenuAction? get_menu_action_at (double px, double py) {+ double n = 0;+ double ix, iy;+ + foreach (MenuAction item in actions) {+ ix = x - 100 + 12;+ + if (direction == MenuDirection.DROP_DOWN) {+ iy = y + 12 + n * item_height;+ } else {+ iy = y - 24 - n * item_height;+ }+ + if (ix <= px <= ix + 100 && iy <= py <= iy + item_height) {+ return item;+ }+ + n++; + }++ return null;+ }+ + public void set_position (double px, double py) {+ x = px;+ y = py;+ }+ + public void draw_menu (Context cr) {+ double ix, iy;+ int n;+ + if (likely (!menu_visible)) {+ return;+ }+ + cr.save ();+ cr.set_source_rgba (122/255.0, 150/255.0, 169/255.0, 1);+ cr.set_line_join (LineJoin.ROUND);+ cr.set_line_width (12);+ + if (direction == MenuDirection.DROP_DOWN)+ cr.rectangle (x - 100 + 18, y + 18, 88, actions.length () * item_height - 12);+ else+ cr.rectangle (x - 100 + 18, y + 6 - actions.length () * item_height, 88, actions.length () * item_height - 12);+ + cr.fill_preserve ();+ cr.stroke ();+ cr.restore ();+ + cr.save ();+ + n = 0;+ foreach (MenuAction item in actions) {+ + if (direction == MenuDirection.DROP_DOWN)+ iy = y + 33 + n * item_height - 5;+ else + iy = y - 8 - n * item_height;+ + ix = x - 100 + 20;+ + item.draw (ix, iy, cr);+ n++;+ }+ + cr.restore ();+ }+ + public void draw_icon (Context cr) {+ double alpha = 1;+ ImageSurface i = (!) icon;+ + if (!menu_visible) {+ alpha = 0;+ }+ + cr.save ();+ + cr.set_source_rgba (122/255.0, 150/255.0, 169/255.0, alpha);+ + cr.rectangle (x, y, 12, 12);+ cr.fill_preserve ();+ cr.stroke ();+ + if (likely (icon != null && i.status () == Cairo.Status.SUCCESS)) {+ cr.set_source_surface (i, x, y + (12 - i.get_height ()) / 2.0);+ cr.paint ();+ } else {+ warning ("Failed to load icon.");+ }+ + cr.restore ();+ }+}++class MenuAction : GLib.Object {+ public string label;+ public DropMenu.Selected action;+ public DropMenu? parent = null;+ public int index = -1;+ bool selected = false;+ + public MenuAction (string label) {+ this.label = label;+ }+ + public void set_selected (bool s) {+ selected = s;+ }+ + public virtual void draw (double x, double y, Context cr) {+ if (selected) {+ cr.save ();+ cr.set_line_join (LineJoin.ROUND);+ cr.set_line_width (12);+ cr.set_source_rgba (102/255.0, 120/255.0, 149/255.0, 1);+ cr.rectangle (x - 2, y - 9, 88, 8);+ cr.fill_preserve ();+ cr.stroke ();+ cr.restore (); + }+ + cr.save ();+ cr.set_source_rgba (1, 1, 1, 1);+ + cr.set_font_size (12);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.NORMAL);+ + cr.move_to (x, y);++ cr.show_text (label);+ cr.restore ();+ }+}++}new file mode 100644@@ -0,0 +1,279 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {++enum PointType {+ NORMAL,+ LINE,+ CURVE,+ DOUBLE_CURVE,+ END,+ FLOATING+}++enum PointDirection {+ NONE,+ UP_RIGHT,+ DOWN_RIGHT,+ UP_LEFT,+ DOWN_LEFT+}++class EditPoint {+ public double x;+ public double y;+ public PointType type;+ + public double r = 1;+ public double g = 0;+ public double b = 0;+ public double a = 1;+ + public unowned List<EditPoint>? prev = null;+ public unowned List<EditPoint>? next = null;++ public PointDirection direction = PointDirection.NONE;+ + public bool active = false;+ bool active_handle = false;+ + public int selected_handle = 0;+ + public EditPointHandle right_handle;+ public EditPointHandle left_handle;+ + public bool tie_handles = false;+ + public EditPoint (double nx = 0, double ny = 0, PointType nt = PointType.LINE) {+ x = nx;+ y = ny;+ type = nt;+ active = false;+ + set_active (true);+ + if (nt == PointType.FLOATING) {+ r = 1;+ g = 1;+ b = 0;+ a = 1;+ active = false;+ }+ + right_handle = new EditPointHandle (this, 0, 7);+ left_handle = new EditPointHandle (this, PI, 7);+ }++ public void set_point_type (PointType point_type) {+ type = point_type;+ }++ public EditPoint copy () {+ EditPoint new_point = new EditPoint ();+ + new_point.x = x;+ new_point.y = y;+ + new_point.type = type;+ new_point.direction = direction;+ + new_point.set_color (r, g, b, a);+ + new_point.tie_handles = tie_handles;+ + new_point.right_handle.angle = right_handle.angle;+ new_point.right_handle.length = right_handle.length;++ new_point.left_handle.angle = left_handle.angle;+ new_point.left_handle.length = left_handle.length; ++ return new_point;+ }++ public bool get_active_handle () {+ return active_handle; + }+ + public void set_active_handle (bool a) {+ active_handle = a; + }+ + public bool is_close (double x, double y) {+ return get_close_distance (x, y) < double.MAX;+ }++ public double get_close_distance (double x, double y, double m = 15) {+ Glyph g = MainWindow.get_current_glyph ();+ + double xt, yt, d;+ double ivz = 1 / g.view_zoom;+ + double xc = (g.allocation.width / 2.0);+ double yc = (g.allocation.height / 2.0);+ + m /= g.view_zoom;+ + x *= ivz;+ y *= ivz;++ xt = x - xc + g.view_offset_x;+ yt = yc - y - g.view_offset_y; + + d = Math.sqrt (Math.pow (this.x - xt, 2) + Math.pow (this.y - yt, 2));+ + if (d < m) return d;+ + return double.MAX;+ }++ public EditPointHandle get_left_handle () {+ return left_handle;+ }+ + public EditPointHandle get_right_handle () {+ return right_handle;+ }+ + public void set_color (double r, double g, double b, double a) {+ this.r = r;+ this.g = g;+ this.b = b;+ this.a = a;+ }+ + public unowned List<EditPoint> get_prev () {+ if (prev == null && next != null && get_next ().prev == get_next ().first ()) {+ prev = get_next ().last ();+ }+ + if (unlikely (prev == null)) {+ warning ("EditPoint.prev is null");+ }+ + return (!) prev;+ }++ public unowned List<EditPoint> get_next () {+ if (unlikely (next == null)) {+ warning ("EditPoint.next is null");+ }+ + return (!) next;+ }+ + public unowned List<EditPoint> get_list () {+ return get_next ().first ();+ }+ + public void set_position (double tx, double ty) {+ x = tx;+ y = ty;+ }+ + public static void to_coordinate (ref double x, ref double y) {+ double xc, yc, xt, yt, ivz;+ Glyph g = MainWindow.get_current_glyph ();+ + ivz = 1 / g.view_zoom;+ + xc = (g.allocation.width / 2.0);+ yc = (g.allocation.height / 2.0);+ + x *= ivz;+ y *= ivz;++ xt = x - xc + g.view_offset_x;+ yt = yc - y - g.view_offset_y; + + x = xt;+ y = yt;+ }+ + public bool set_active (bool active) {+ bool update = (this.active != active);+ + if (update) {+ this.active = active;+ + if (active) {+ r = 0.5;+ g = 0;+ b = 1;+ a = 1;+ } else {+ r = 0;+ g = 1;+ b = 0.5;+ a = 1;+ }+ }+ + return update;+ }+ + // Fixa: ta bort färgmarkeringarna+ public void set_direction () {+ EditPoint pep = this.get_next ().data;+ EditPoint ep = this;++ if (pep.x < ep.x) {+ + if (pep.y < ep.y) { + ep.direction = PointDirection.DOWN_LEFT;+ ep.set_color (1,1,0,1); // gul + } else {+ ep.direction = PointDirection.UP_LEFT;+ ep.set_color (1,0,0,1); // röd+ }+ + } else {++ if (pep.y < ep.y) {+ ep.direction = PointDirection.DOWN_RIGHT;+ ep.set_color (0,1,0,1); // grön+ } else {+ ep.direction = PointDirection.UP_RIGHT;+ ep.set_color (0,1,1,1); // ljusblå+ }+ + } + }+ + public int get_index () {+ int i = -1;+ + if (next == null) return i;+ + foreach (var d in get_next ().first ()) {+ i++;+ + if (this == d) {+ return i;+ }+ }+ + return i;+ }+}++}new file mode 100644@@ -0,0 +1,137 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {+ +class EditPointHandle {+ + public double angle;+ public double length;+ + public unowned EditPoint parent;+ + EditPoint? visual_handle = null;++ static EditPoint none = new EditPoint ();++ public EditPointHandle.empty() {+ this.parent = none;+ this.angle = 0;+ this.length = 10;+ }+ + public EditPointHandle (EditPoint parent, double angle, double length) {+ this.parent = parent;+ this.angle = angle;+ this.length = length;+ }++ public double x () {+ double r = px ();+ + if (unlikely (r <= -100000)) {+ print_position ();+ }+ + return r;+ }+ + public double y () {+ double r = py ();+ + if (unlikely (r <= -100000)) {+ print_position ();+ }+ + return r;+ }+ + double px () {+ return cos (angle) * length + parent.x;+ }++ double py () {+ return sin (angle) * length + parent.y;+ }+ + void print_position () {+ warning (@"\nEdit point handle at position $(px ()),$(py ()) is not valid.\n"+ + @"Type: $(parent.type), "+ + @"Index: $(parent.get_index ()) of $(parent.get_list ().length ())\n"+ + @"Angle: $angle, Length: $length."); + }+ + public EditPoint get_point () {+ EditPoint p;+ + if (visual_handle == null) {+ visual_handle = new EditPoint (0, 0);+ }+ + p = (!) visual_handle;+ p.x = x ();+ p.y = y ();+ + return p;+ }+ + public void move_to_coordinate (double x, double y) {+ double a, b, c;++ a = parent.x - x;+ b = parent.y - y;+ c = a * a + b * b;+ + if (c == 0) {+ return;+ }+ + length = sqrt (fabs (c)); + + if (c < 0) length = -length;+ + if (y < parent.y) {+ angle = acos (a / length) + PI;+ } else {+ angle = -acos (a / length) + PI;+ }+ + if (parent.tie_handles) {+ tie_handle ();+ }+ }++ private void tie_handle () {+ if (this == parent.get_left_handle ()) {+ parent.get_right_handle ().angle = angle - PI;+ } else {+ parent.get_left_handle ().angle = angle - PI;+ }+ }+ + public void move_to (double x, double y) {+ EditPoint.to_coordinate (ref x, ref y);+ move_to_coordinate (x, y);+ }+}++}new file mode 100644@@ -0,0 +1,356 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class ExportTool : Tool {++ private static string? prefered_browser = null;++ public ExportTool (string n) {+ base (n, "Export glyph to svg file", 'e', CTRL);+ + select_action.connect((self) => {+ export_svg_font ();+ // export_otf_font ();+ // Fixa: add export_glyph_to_svg (); as tool in toolbox+ });+ + press_action.connect((self, b, x, y) => {+ });++ release_action.connect((self, b, x, y) => {+ });+ + move_action.connect ((self, x, y) => {+ });+ }++ public void export_glyph_to_svg () {+ Glyph glyph = MainWindow.get_current_glyph ();+ + Font font = Supplement.get_current_font ();+ FontDisplay fd = MainWindow.get_current_display ();+ + string glyph_svg;+ string svg_file = @"$(glyph.get_name ()).svg";+ + File file = font.get_folder ().get_child (svg_file);+ + DataOutputStream os;++ string name = glyph.get_name ();+ + int pid = 0;+ + TooltipArea status = MainWindow.get_tool_tip ();+ + if (!(fd is Glyph)) {+ return;+ }+ + print (@"Writing file $((!)file.get_path ())\n");+ + try {+ + if (file.query_exists ()) {+ file.delete ();+ }+ + os = new DataOutputStream (file.create(FileCreateFlags.REPLACE_DESTINATION)); + + os.put_string (+"""<?xml version="1.0" standalone="no"?>+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >+<svg """);++ os.put_string (@"xmlns=\"http://www.w3.org/2000/svg\"\n");+ os.put_string (@"\twidth=\"$(glyph.get_width () / Glyph.SCALE)\"\n");+ os.put_string (@"\theight=\"$(glyph.get_height () / Glyph.SCALE)\"\n");+ os.put_string (@"\tid=\"glyph_$(name)\"\n");+ os.put_string (@"\tversion=\"1.1\">\n");+ + os.put_string (@"\n");+ + os.put_string (@"<g id=\"$(name)\">\n");+ + foreach (Path p in glyph.path_list) {+ + glyph_svg = Svg.to_svg_path (p, glyph);+ + os.put_string (@"<path ");+ os.put_string (@"style=\"fill:#000000;stroke-width:0px\" ");+ os.put_string (@"d=\"$(glyph_svg)\" id=\"path_$(name)$(pid++)\" />\n");+ + }++ os.put_string ("""</g>+ +</svg>""");+ + } catch (Error e) {+ stderr.printf (@"Export \"$svg_file\" \n");+ critical (@"$(e.message)");+ status.show_text ("Can't export $svg_file.");+ }+ + status.show_text ("Wrote $svg_file.");+ }++ /** Export font and open html document */+ public void view_result () {+ Font font = Supplement.get_current_font ();+ string path = @"$(font.get_name ()).html";+ File dir = font.get_folder ();+ File file = dir.get_child (path);+ string browser = ""; + + if (!export_svg_font ()) {+ warning ("Failed to export font.");+ return;+ }+ + try { + if (!file.query_exists ()) {+ generate_html_document ((!)file.get_path (), font); + }++ browser = find_browser ();+ Process.spawn_command_line_async (@"$browser $(file.get_uri ())");+ } catch (Error e) {+ stderr.printf (@"Failed to execute \"$browser $path\" \n");+ critical (@"$(e.message)");+ }+ + }+ + private string find_browser () { + File f;+ string b;+ if (prefered_browser != null) return (!) prefered_browser;+ + b = @"C:\\Users\\$(Environment.get_user_name())\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"; // mind the hyphen+ + f = File.new_for_path (b);+ if (f.query_exists ()) {+ prefered_browser = @"'$b'"; // hyphen for windows command+ return (!) prefered_browser;+ }+ + prefered_browser = "sensible-browser";+ return (!) prefered_browser;+ }+ + public void generate_html_document (string html_file, Font font) throws Error {+ File file = File.new_for_path (html_file);+ DataOutputStream os = new DataOutputStream (file.create(FileCreateFlags.REPLACE_DESTINATION));++ string name = font.get_name ();++ os.put_string (+"""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">+<html>+<head>++ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">+ <title>Lorem ipsum</title>+ + <style type="text/css">++ @font-face{+ font-family: '"""); os.put_string (@"$(name)"); os.put_string ("""';+ src: url('"""); os.put_string (@"$(name).svg#$(name)"); os.put_string ("""') format('svg');+ }++ h1 {+ margin: 0 0 5px 0;+ color: #afafaf;+ }++ h2 {+ margin: 0 0 5px 0;+ color: #afafaf;+ }+ + h3 {+ margin: 0 0 5px 0;+ color: #afafaf;+ }+ + h4 {+ margin: 0 0 5px 0;+ color: #afafaf;+ }+ + p {+ margin: 0 0 5px 0;+ color: #000000;+ }+ + body {+ margin: 30px 0 0 30px;+ }+ + div {+ font-family: '"""); os.put_string (@"$(name)"); os.put_string ("""';+ margin: 0 0 30px 0;+ }++ h1.bigger {+ font-size: 58pt;+ }++ h2.big {+ font-size: 40pt;+ }+ + h3.small {+ font-size: 32pt;+ }++ h4.smaller {+ font-size: 18pt;+ }++ h1.percent {+ font-size: 100%;+ } + + p.bigger {+ font-size: 32pt;+ }++ p.big {+ font-size: 24pt;+ }+ + p.small {+ font-size: 18pt;+ }++ p.smaller {+ font-size: 12pt;+ }++ </style>+""");+ + os.put_string (+""" +</head>+<body>++<div>+ <h1 class="bigger">Lorem ipsum</h1>+ <p class="bigger">Dolor sit amet!</p>+</div>++<div>+ <h2 class="big">Hamburgerfonstiv</h2>+ <p class="big">Inspirerande</p>+</div>++<div>+ <h3 class="small">Handgloves & Mittoms</h3>+ <p class="small">Wind blows in Hauges poetry.</p>+</p>+</div>++<div>+ <h4 class="smaller">Headline 16pt</h4>+ <p class="smaller">Ett litet stycke svalhjärta.</p> +</div>++<div>+ <h4 class="smaller">Alphabet</h4>+ <p class="smaller">a b c d e f g h i j k l m n o p q r s t u v w x y z å ä ö</p>+ <p class="smaller">A B C D E F G H I J K L M N O P Q R S T U V W X Y Z Å Ä Ö</p>+ <p class="smaller">0 1 2 3 4 5 6 7 8 9</p>+</div>++</body>+</html>+""");++ }++ public bool export_otf_font () {+ try {+ Font font = Supplement.get_current_font ();+ File file = font.get_folder ();+ file = file.get_child (font.get_name () + ".otf");+ OpenFontFormatWriter fo;+ + if (file.query_exists ()) {+ file.delete ();+ }+ + fo = new OpenFontFormatWriter ();+ fo.open (file);+ fo.write_font_file (font);+ fo.close ();+ + } catch (Error e) {+ critical (@"$(e.message)");+ return false;+ }+ + return true;+ }+ + public bool export_svg_font () {+ TooltipArea ta = MainWindow.get_tool_tip ();+ Font font = Supplement.get_current_font ();+ string file_name = @"$(font.get_name ()).svg";+ File file;+ SvgFontFormatWriter fo;+ + try {+ file = font.get_folder ();+ file = file.get_child (file_name);+ + if (file.query_exists ()) {+ stderr.printf (@"ExportTool: Output file (\"$((!) file.get_path ())\") exists. Deleting it.\n");+ file.delete ();+ }+ + fo = new SvgFontFormatWriter ();+ fo.open (file);+ fo.write_font_file (font);+ fo.close ();+ } catch (Error e) {+ critical (@"$(e.message)");+ ta.show_text (e.message);+ return false;+ }+ + ta.show_text (@"Wrote $file_name");+ return true;+ }+ + public override bool test () {+ this.yield ();+ return true;+ }++ public void test_view_result () {+ test ();+ view_result ();+ }+}++}new file mode 100644@@ -0,0 +1,337 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;+using Gdk;+using Cairo;++namespace Supplement {++class FileSelection {+ + public double x = 0;+ public double y = 0;+ + public StringBuilder path;+ + public bool show_marker = false;+ public bool input = false;+ + public signal void redraw ();+ + bool running;+ List<string> directory_listing = new List<string> ();+ int list_offset = 0;+ int list_n_rows = 0;+ + string sep = "/";+ + int has_modifier = 0;+ + List<string> listing;+ + public FileSelection () {+ path = new StringBuilder ();+ path.append (Supplement.get_current_font ().get_path ());+ running = true;+ + sep = (path.str.index_of ("/") == 0) ? "/" : "\\"; // windows and unix sepparators+ + var time = new TimeoutSource(600);+ + time.set_callback (() => {+ + if (input) show_marker = true;+ else show_marker = !show_marker;+ + input = false;+ + redraw ();+ return running;+ });++ time.attach (null);+ }+ + public void set_path (string fn) {+ path = new StringBuilder ();+ path.append (fn);+ }+ + public void draw (Allocation allocation, Context cr) {+ FontExtents fe;+ + cr.save ();+ cr.move_to (x, y);+ + cr.set_font_size (12);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + path.append ((show_marker) ? "|" : " ");+ cr.font_extents (out fe);+ + // Fixa: pango font metrix vore bra+ if (allocation.width / (1 * fe.max_x_advance) < path.len) {+ int t = path.str.last_index_of (sep) ;+ string s;+ + return_if_fail (t != -1);+ s = path.str.substring (t + 1);+ cr.show_text (s);+ } else {+ cr.show_text (path.str);+ }+ path.truncate (path.len - 1);+ + cr.stroke ();+ cr.restore ();+ + draw_directory_listing (allocation, cr);+ }++ void draw_directory_listing (Allocation allocation, Context cr) {+ int lh = 90;+ int n = 0;+ int m = (int) ((allocation.height - lh) / 50.0);+ int skip = list_offset;+ int max_fn_len = 36;+ + list_n_rows = m;++ cr.save ();++ cr.set_font_size (12);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + cr.move_to (x, y);+ + foreach (var f in directory_listing) {+ if (++n >= m + list_offset) continue; // belowe+ if (--skip > 0) continue; // above+ + lh += 25;+ cr.move_to (x, y + lh);+ + if (f.char_count () > max_fn_len) {+ f = f.substring (0, max_fn_len);+ f += " ...";+ }+ + cr.show_text (f);+ }++ if (n >= m && n != 0 && n - m > 0) {+ lh += 25;+ cr.move_to (x, y + lh);+ cr.show_text (@"and $(n - m) more files");+ }+ + cr.stroke ();+ cr.restore ();+ }+ + public void key_press (EventKey e) {+ unichar c;+ int max;+ + show_marker = true;+ input = true;+ + if (is_mod (e)) {+ has_modifier++;+ }+ + if (e.keyval == Key.DOWN || e.keyval == Key.UP ) {+ list_offset += (e.keyval == Key.DOWN) ? +1 : -1;+ + max = (int) directory_listing.length () - list_n_rows;+ if (list_offset > max) {+ list_offset = max;+ }++ if (list_offset < 0) {+ list_offset = 0;+ }+ + } else if (e.keyval == Key.BACK_SPACE) { + path.truncate (path.len - 1);+ + while (!path.str.validate ()) {+ path.truncate (path.len - 1);+ + if (path.str.length == 0) {+ break;+ }+ }+ + } else if (e.keyval == Key.TAB) {+ autocomplete ();+ } else {+ c = (unichar) e.keyval;+ + // don't write .ffis instead of .ffi+ if (c == 's' && path.str.substring (-4) == ".ffi") {+ return;+ }+ + if (has_modifier == 0 && !is_modifier_key (e.keyval) && c.validate ()) {+ path.append_unichar (c);+ }+ }+ }+ + public void key_release (EventKey e) {+ if (is_mod (e)) {+ has_modifier--;+ }+ }+ + public void autocomplete () {+ + if (path.str.length == 0) {+ path.append (sep);+ return;+ }+ + try {+ int p = path.str.last_index_of (sep);++ if (p == -1) return;++ bool list_all = false;+ File dd = File.new_for_path (path.str);+ bool matching = false;+ + listing = new List<string> ();+ + if (dd.query_file_type (0) == FileType.DIRECTORY) {+ if (path.str.substring (-1) != sep)+ path.append (sep);+ else+ list_all = true; // and print all files+ }++ string d = path.str.substring (0, p + 1);+ string f = path.str.substring (p + 1);+ File directory = File.new_for_path (d);+ FileEnumerator enumerator = directory.enumerate_children (FILE_ATTRIBUTE_STANDARD_NAME, 0); + FileInfo? file_info;+ FileInfo fi;+ + while ((file_info = enumerator.next_file ()) != null) {+ fi = (!) file_info;+ + if (fi.get_name ().index_of (".") == 0) continue;+ + matching = (fi.get_name ().index_of (f) == 0);+ + if (list_all || fi.get_name ().index_of (f) == 0) { // match+ StringBuilder nfn = new StringBuilder ();+ nfn.append (fi.get_name ());++ if (!nfn.str.validate ()) continue;++ if (fi.get_file_type () == FileType.DIRECTORY) { // Fixa: filetype is G_FILE_TYPE_UNKNOWN+ nfn.append (sep);+ listing.append (nfn.str);+ } else if (fi.get_name ().index_of (".ffi") != -1) {+ listing.append (nfn.str);+ } else {+ listing.append (nfn.str);+ }+ }+ }++ // as complete it can be do nothing+ if (listing.length () == 0) {+ return;+ }+ + // clear previous listing+ while (directory_listing.length () > 0) {+ directory_listing.delete_link (directory_listing.first ());+ }++ // unique auto complete path+ if (listing.length () == 1) {+ path.erase ();+ path.append (d);+ path.append (listing.first ().data);+ }+ + // matching but not unique+ partly_complete ();+ + // show directory content+ list_offset = 0;+ if (listing.length () > 1) {+ foreach (var ldf in listing) {+ directory_listing.append (ldf);+ }+ }+ + } catch (Error e) {+ }+ + }+ + private void partly_complete () { + int p, i;+ string dir;+ + p = path.str.last_index_of (sep);+ dir = path.str.substring (0, p + 1);++ string new_file_name;+ bool variation;+ + if (listing.length () == 0) {+ return;+ }+ + new_file_name = listing.first ().data;+ + while (new_file_name.length > 1) {+ + variation = false;+ + foreach (string listed in listing) {+ i = listed.index_of (new_file_name);++ if (i == -1) {+ variation = true;+ }+ }+ + if (variation) {+ new_file_name = new_file_name.substring (0, new_file_name.length - 1);+ } else {+ + path.erase ();+ path.append (dir);+ path.append (new_file_name);+ + return;+ }+ }++ return;+ }+ +}++}new file mode 100644@@ -0,0 +1,1002 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Xml;++namespace Supplement {++class Font : GLib.Object {+ + HashTable <string, GlyphCollection> glyph_cache = new HashTable <string, GlyphCollection> (str_hash, str_equal);+ public HashTable <string, Kerning> kerning = new HashTable <string, Kerning> (str_hash, str_equal);+ + public List <string> background_images = new List <string> ();+ + /** Top margin */+ public double top_limit;+ + /** Height of upper case letters. */+ public double top_position;++ /** x-height upper bearing from origo. */+ public double xheight_position;++ /** Base line coordinate from origo. */+ public double base_line;+ + /** Descender position */+ public double bottom_position;+ + /** Bottom margon */+ public double bottom_limit;+ + public string? backup_file = null;+ public string? font_file = null;+ + bool modified = false;+ + private string? svg_thumbnail = null;+ + private string name = "typeface";+ + public Font () {+ // positions in pixels at first zoom level+ // default x-height should be 60 in 1:1+ top_limit = -66 * Glyph.SCALE;+ top_position = -54 * Glyph.SCALE;+ xheight_position = -38 * Glyph.SCALE;+ base_line = 18 * Glyph.SCALE;+ bottom_position = 38 * Glyph.SCALE;+ bottom_limit = 45 * Glyph.SCALE;+ }++ public void touch () {+ modified = true;+ }++ public File get_backgrounds_folder () {+ string fn = @"$(get_name ()) backgrounds";+ File f = Supplement.get_settings_directory ().get_child (fn);+ return f;+ }++ /** Retuns true if the current font has be modified */+ public bool is_modified () {+ return modified;+ }++ /** Full path to this font file. */+ public string get_path () {+ int i = 0;+ + if (font_file != null) {+ return (!) font_file;+ }+ + StringBuilder sb = new StringBuilder ();+ sb.append (Environment.get_home_dir ());+ sb.append (@"/$(get_name ()).ffi");+ + File f = File.new_for_path (sb.str);++ while (f.query_exists ()) {+ sb.erase ();+ sb.append (Environment.get_home_dir ());+ sb.append (@"/$(get_name ())$(++i).ffi");+ f = File.new_for_path (sb.str);+ }+ + return sb.str;+ }+ + public File get_folder () {+ string p = get_path ();+ int i = p.last_index_of ("/");+ + if (i == -1) {+ i = p.last_index_of ("\\");+ }+ + p = p.substring (0, i);+ + return File.new_for_path (p);+ }++ public string get_svg_thumbnail () {+ if (svg_thumbnail == null) {+ svg_thumbnail = load_svg_thumbnail ();+ }+ + return (!) svg_thumbnail;+ }+ + public string load_svg_thumbnail () {+ File file;+ string line;+ string? l;+ DataInputStream dis;+ + int start, stop, len;+ + if (unlikely (font_file == null)) {+ warning (@"File is null in load_svg_thumbnail.\n");+ return "";+ }+ + file = File.new_for_path ((!)font_file);+ + if (unlikely (!file.query_exists ())) {+ warning (@"File '$((!)font_file)' doesn't exist.\n");+ return "";+ }++ try {+ dis = new DataInputStream (file.read ());+ + while ((l = dis.read_line (null)) != null) {+ + assert (l != null);+ + line = (!) l;+ + if (likely (line.index_of ("<thumbnail svg=\"") == 0)) {+ start = line.index_of ("\"");+ stop = line.last_index_of ("\"");+ + if (unlikely (start == -1 || stop == -1)) {+ warning ("No delimter found for line: $line\n");+ return "";+ }+ + len = stop - start;+ + return line.substring (start, len);+ }+ }+ + } catch (GLib.Error e) {+ warning (@"$(e.message)");+ }+ + warning (@"No thumbnail found in '$((!)font_file)'");+ + return "";+ }+ + public string get_new_svg_thumbnail () {+ Glyph? g;+ Glyph glyph;+ + g = get_glyph ("a");+ + if (is_valid_thumbnail (g)) {+ glyph = (!) g;+ return glyph.get_svg_data ();+ }+ + for (int i = 0; i < 100; i++) {+ g = get_glyph_indice (i);+ + if (is_valid_thumbnail (g)) {+ glyph = (!) g;+ return glyph.get_svg_data ();+ }+ }+ + return "";+ }+ + bool is_valid_thumbnail (Glyph? g) {+ Glyph glyph;+ double x1, x2, y1, y2;++ if (g == null) {+ return false;+ }+ + glyph = (!) g;+ + glyph.boundries (out x1, out y1, out x2, out y2);+ + if (x1 < -120) return false;+ if (x2 > 120) return false;+ if (y1 < -76) return false;+ if (y2 > 76) return false;+ + if (x1 == 0 && x2 == 0 && y1 == 0 && y2 == 0) return false;+ + return true;+ }+ + public double get_height () {+ double r = base_line - top_position;+ return (r > 0) ? r : -r;+ }+ + public void set_name (string name) {+ this.name = name;+ }+ + public string get_name () {+ return name;+ }++ public GlyphRange get_available_glyph_ranges () {+ GlyphRange gr = new GlyphRange ();+ + Glyph? g = null;+ unichar i = 0;+ + while ((g = get_glyph_indice (i++)) != null) {+ gr.add_single (((!) g).get_unichar ());+ }+ + gr.sort ();+ + return gr;+ }+ + public void add_glyph (Glyph glyph) {+ unowned GlyphCollection? gc = glyph_cache.lookup (glyph.get_name ());++ if (gc == null) {+ glyph_cache.insert (glyph.get_name (), new GlyphCollection (glyph));+ }+ }++ public void add_glyph_collection (GlyphCollection glyph_collection) {+ unowned GlyphCollection? gc = glyph_cache.lookup (glyph_collection.get_name ());++ if (unlikely (gc != null)) {+ warning ("Glyph collection exists.");+ }+ + glyph_cache.insert (glyph_collection.get_name (), glyph_collection);+ }+ + public void delete_glyph (string glyph) {+ glyph_cache.remove (glyph);+ }+ + /** Obtain all version and alterntes for this glyph. */+ public GlyphCollection? get_glyph_collection (string glyph) {+ return glyph_cache.lookup (glyph);+ }+ + public Glyph? get_glyph (string glyph) {+ unowned GlyphCollection? gc = glyph_cache.lookup (glyph);+ + if (gc == null) {+ return null;+ }+ + return ((!)gc).get_current ();+ }+ + public Glyph? get_glyph_indice (unichar glyph_indice) {+ List<unowned GlyphCollection> gl;+ + if (glyph_indice >= glyph_cache.size ()) {+ return null;+ }+ + gl = glyph_cache.get_values ();+ + return gl.nth (glyph_indice).data.get_current ();+ }+ + public void add_background_image (string file) {+ background_images.append (file);+ }++ public double get_kerning (string a, string b) {+ Kerning? kern;+ StringBuilder key = new StringBuilder ();+ + key.append (a);+ key.append (b);+ + kern = kerning.lookup (key.str);+ + if (kern != null) {+ return ((!) kern).val;+ }+ + return 0;+ }++ public void set_kerning (string a, string b, double val) {+ Kerning? kern;+ Kerning k;+ StringBuilder key = new StringBuilder ();+ + key.append (a);+ key.append (b);+ + kern = kerning.lookup (key.str);+ + if (kern != null) {+ k = (!) kern;+ k.val = val;+ } else {+ k = new Kerning (a, b, val);+ kerning.insert (key.str, k);+ }+ }+ + public void save_backup () {+ File dir = Supplement.get_backup_directory ();+ File temp_file;+ int i = 0;++ if (backup_file == null) {+ temp_file = dir.get_child (@"current_font_$i.ffs");+ + while (temp_file.query_exists ()) {+ i++;+ temp_file = dir.get_child (@"current_font_$i.ffs");+ }+ + backup_file = temp_file.get_path ();+ }+ + write_font_file ((!) backup_file);+ }+ + public bool save (string path) {+ bool r = write_font_file (path);+ + if (r) {+ font_file = path;+ }+ + modified = false;+ + return r;+ }++ public bool write_font_file (string path) {+ try {+ File file = File.new_for_path (path);++ if (file.query_file_type (0) == FileType.DIRECTORY) {+ stderr.printf (@"Can not save font. $path is a directory.");+ return false;+ }+ + if (file.query_exists ()) {+ file.delete ();+ }+ + DataOutputStream os = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION));+ + os.put_string ("""<?xml version="1.0" encoding="utf-8" standalone="yes"?>""");+ os.put_string ("\n");+ + os.put_string ("<font>\n");+ + os.put_string (@"<thumbnail svg=\"$(get_new_svg_thumbnail ())\"/>\n");+ + os.put_string ("\n");+ os.put_string (@"<name>$(get_name ())</name>\n");+ + os.put_string ("\n");+ os.put_string ("<lines>\n");+ + os.put_string (@"\t<top_limit>$top_limit</top_limit>\n");+ os.put_string (@"\t<top_position>$top_position</top_position>\n");+ os.put_string (@"\t<x-height>$xheight_position</x-height>\n");+ os.put_string (@"\t<base_line>$base_line</base_line>\n");+ os.put_string (@"\t<bottom_position>$bottom_position</bottom_position>\n");+ os.put_string (@"\t<bottom_limit>$bottom_limit</bottom_limit>\n");+ + os.put_string ("</lines>\n\n");++ foreach (SpinButton s in GridTool.sizes) {+ os.put_string (@"<grid width=\"$(s.get_display_value ())\"/>\n");+ }+ + if (GridTool.sizes.length () > 0) {+ os.put_string ("\n");+ }+ + os.put_string (@"<background scale=\"$(MainWindow.get_toolbox ().background_scale.get_display_value ())\" />\n");+ os.put_string ("\n");+ + if (background_images.length () > 0) {+ os.put_string (@"<images>\n");+ + foreach (string f in background_images) {+ os.put_string (@"\t<img src=\"$f\"/>\n");+ }+ + os.put_string (@"</images>\n");+ os.put_string ("\n");+ }+ + glyph_cache.for_each ((k, gc) => {+ try {+ bool selected;+ foreach (Glyph g in gc.get_version_list ().glyphs) {+ selected = (g == gc.get_current ());+ write_glyph (g, selected, os);+ }+ } catch (GLib.Error ef) {+ stderr.printf (@"Failed to save $path \n");+ stderr.printf (@"$(ef.message) \n");+ }+ });+ + // FIXME: implement kerning in webkit + kerning.for_each ((key, kern) => {+ try {+ string l = to_hex (kern.left.get_char (0));+ string r = to_hex (kern.right.get_char (0));+ + os.put_string (@"<hkern left=\"$l\" right=\"$r\" kerning=\"$(kern.val)\"/>\n");+ } catch (GLib.Error ef) {+ stderr.printf (@"Failed to save $path \n");+ stderr.printf (@"$(ef.message) \n");+ }+ });+ + os.put_string ("</font>");+ + } catch (GLib.Error e) {+ stderr.printf (@"Failed to save $path \n");+ stderr.printf (@"$(e.message) \n");+ return false;+ }+ + return true;+ }++ private void write_glyph (Glyph g, bool selected, DataOutputStream os) throws GLib.Error {+ os.put_string (@"<glyph unicode=\"$(to_hex (g.unichar_code))\" selected=\"$selected\" left=\"$(g.left_limit)\" right=\"$(g.right_limit)\">\n");++ foreach (var p in g.path_list) {+ if (p.points.length () == 0) {+ continue;+ }+ + os.put_string ("\t<object>");++ foreach (var ep in p.points) {+ os.put_string (@"<point x=\"$(ep.x)\" y=\"$(ep.y)\" ");+ + if (ep.type == PointType.CURVE) {+ os.put_string (@"right_angle=\"$(ep.right_handle.angle)\" ");+ os.put_string (@"right_length=\"$(ep.right_handle.length)\" ");+ os.put_string (@"left_angle=\"$(ep.left_handle.angle)\" ");+ os.put_string (@"left_length=\"$(ep.left_handle.length)\" "); + os.put_string (@"tie_handles=\"$(ep.tie_handles)\" ");+ }+ + os.put_string ("/>");+ + }+ + os.put_string ("</object>\n");+ }+ + GlyphBackgroundImage? bg = g.get_background_image ();+ + if (bg != null) {+ GlyphBackgroundImage background_image = (!) bg;++ double off_x = background_image.img_offset_x;+ double off_y = background_image.img_offset_y;+ + double scale_x = background_image.img_scale_x;+ double scale_y = background_image.img_scale_y;+ + double rotation = background_image.img_rotation;+ + os.put_string (@"\t<background src =\"$(background_image.get_path ())\" offset_x=\"$off_x\" offset_y=\"$off_y\" scale_x=\"$scale_x\" scale_y=\"$scale_y\" rotation=\"$rotation\"/>\n");+ }+ + os.put_string ("</glyph>\n\n"); ++ }++ public void set_font_file (string path) {+ font_file = path;+ modified = false;+ }++ public bool is_empty () {+ uint len = glyph_cache.get_keys ().length ();+ return (len == 0);+ }++ public bool load (string path) {+ set_font_file (path);+ return parse_file (path);+ }++ public bool parse_file (string path) {+ Parser.init ();+ + Xml.Doc* doc = Parser.parse_file (path);+ Xml.Node* root = doc->get_root_element ();++ if (root == null) {+ delete doc;+ return false;+ }++ // set this path as file for this font+ font_file = path;+ + // empty cache and fill it with new glyphs from disk+ glyph_cache.remove_all ();+ MainWindow.get_toolbox ().remove_all_grid_buttons ();+ while (background_images.length () > 0) {+ background_images.remove_link (background_images.first ());+ }++ Xml.Node* node = root;+ for (Xml.Node* iter = node->children; iter != null; iter = iter->next) {+ + if (iter->name == "glyph") {+ parse_glyph (iter);+ }++ if (iter->name == "lines") {+ parse_font_boundries (iter);+ }++ if (iter->name == "grid") {+ parse_grid (iter);+ }++ if (iter->name == "background") {+ parse_background (iter);+ }+ + if (iter->name == "images") {+ parse_background_selection (iter);+ }++ if (iter->name == "name") {+ name = iter->children->content;+ }++ if (iter->name == "hkern") {+ parse_kerning (iter);+ }+ }+ + delete doc;+ Parser.cleanup ();++ return true;+ }+ + private void parse_kerning (Xml.Node* node) {+ string attr_name;+ string attr_content;+ string left = "";+ string right = "";+ string kern = "";+ Kerning k;+ StringBuilder b;+ + + for (Xml.Attr* prop = node->properties; prop != null; prop = prop->next) {+ attr_name = prop->name;+ attr_content = prop->children->content;+ + if (attr_name == "left") {+ b = new StringBuilder ();+ b.append_unichar (to_unichar (attr_content));+ left = @"$(b.str)";+ }++ if (attr_name == "right") {+ b = new StringBuilder ();+ b.append_unichar (to_unichar (attr_content));+ right = @"$(b.str)";+ }+ + if (attr_name == "kerning") {+ kern = attr_content;+ }+ }+ + k = new Kerning.from_attribute (left, right, kern);+ + this.kerning.insert (@"$left$right", k);+ }+ + private void parse_background (Xml.Node* node) {+ string attr_name;+ string attr_content;+ + for (Xml.Attr* prop = node->properties; prop != null; prop = prop->next) {+ attr_name = prop->name;+ attr_content = prop->children->content;+ + if (attr_name == "scale") {+ MainWindow.get_toolbox ().background_scale.set_value (attr_content);+ }+ }+ }+ + private void parse_background_selection (Xml.Node* node) {+ string attr_name = "";+ string attr_content;+ + return_if_fail (node != null);+ + for (Xml.Node* iter = node->children; iter != null; iter = iter->next) {+ if (iter->name == "img") {+ for (Xml.Attr* prop = iter->properties; prop != null; prop = prop->next) {+ attr_name = prop->name;+ attr_content = prop->children->content;+ + if (attr_name == "src") {+ background_images.append (attr_content);+ }+ }+ }+ }+ }+ + private void parse_grid (Xml.Node* node) {+ for (Xml.Attr* prop = node->properties; prop != null; prop = prop->next) {+ string attr_name = prop->name;+ string attr_content = prop->children->content;+ + if (attr_name == "width") {+ MainWindow.get_toolbox ().parse_grid (attr_content);+ }+ } + }+ + private void parse_font_boundries (Xml.Node* node) {+ for (Xml.Node* iter = node->children; iter != null; iter = iter->next) {+ if (iter->name == "top_limit") top_limit = parse_double_from_node (iter);+ if (iter->name == "top_position") top_position = parse_double_from_node (iter);+ if (iter->name == "x-heigh") xheight_position = parse_double_from_node (iter);+ if (iter->name == "base_line") base_line = parse_double_from_node (iter);+ if (iter->name == "bottom_position") bottom_position = parse_double_from_node (iter);+ if (iter->name == "bottom_limit") bottom_limit = parse_double_from_node (iter);+ } + }+ + private double parse_double_from_node (Xml.Node* iter) {+ double d;+ bool r = double.try_parse (iter->children->content, out d);+ + if (unlikely (!r)) {+ string? s = iter->content;+ if (s == null) warning (@"Content is null for node $(iter->name)\n");+ else warning (@"Failed to parse double for \"$(iter->content)\"\n");+ }+ + return (r) ? d : 0;+ }+ + /** Parse one glyph. */+ public void parse_glyph (Xml.Node* node) {+ string name = "";+ int left = 0;+ int right = 0;+ unichar uni = 0;+ int version = 0;+ bool selected = false;+ Glyph g;+ GlyphCollection? gc;+ + for (Xml.Attr* prop = node->properties; prop != null; prop = prop->next) {+ string attr_name = prop->name;+ string attr_content = prop->children->content;+ StringBuilder b;+ + if (attr_name == "unicode") {+ uni = to_unichar (attr_content);+ b = new StringBuilder ();+ b.append_unichar (uni);+ name = b.str;+ }++ if (attr_name == "left") {+ left = int.parse (attr_content);+ }+ + if (attr_name == "right") {+ right = int.parse (attr_content);+ }+ + if (attr_name == "version") {+ version = int.parse (attr_content);+ }+ + if (attr_name == "selected") {+ selected = bool.parse (attr_content);+ }+ }++ g = new Glyph (name, uni);+ + g.left_limit = left;+ g.right_limit = right;++ parse_content (g, node);+ + // use disk thread and idle add this:+ + gc = get_glyph_collection (g.get_name ());+ + if (gc == null) {+ gc = new GlyphCollection ();+ glyph_cache.insert (g.get_name (), (!) gc);+ }+ + ((!)gc).insert_glyph (g, selected);+ }+ + /** Parse visual objects and paths */+ private void parse_content (Glyph g, Xml.Node* node) {+ Xml.Node* i;+ return_if_fail (node != null);+ + for (Xml.Node* iter = node->children; iter != null; iter = iter->next) {+ if (iter->name == "object") {+ Path p = new Path ();+ + for (i = iter->children; i != null; i = i->next) {+ if (i->name == "point") {+ parse_point (p, i);+ } + }++ p.close ();+ g.add_path (p);+ }+ + if (iter->name == "background") {+ parse_background_scale (g, iter);+ }+ }+ }+ + private void parse_background_scale (Glyph g, Xml.Node* node) {+ GlyphBackgroundImage img = new GlyphBackgroundImage ();+ + string attr_name;+ string attr_content;+ + for (Xml.Attr* prop = node->properties; prop != null; prop = prop->next) {+ attr_name = prop->name;+ attr_content = prop->children->content;+ + if (attr_name == "src") {+ img = new GlyphBackgroundImage (attr_content);+ g.set_background_image (img);+ }+ }+ + for (Xml.Attr* prop = node->properties; prop != null; prop = prop->next) {+ attr_name = prop->name;+ attr_content = prop->children->content;+ + if (attr_name == "offset_x") {+ img.img_offset_x = double.parse (attr_content);+ }++ if (attr_name == "offset_y") {+ img.img_offset_y = double.parse (attr_content);+ }+ + if (attr_name == "scale_x") {+ img.img_scale_x = double.parse (attr_content);+ }+ + if (attr_name == "scale_y") {+ img.img_scale_y = double.parse (attr_content);+ } + + if (attr_name == "rotation") {+ img.img_rotation = double.parse (attr_content);+ }+ }+ + }+ + private void parse_point (Path p, Xml.Node* iter) {+ double x = 0;+ double y = 0;+ + PointType type = PointType.LINE;+ + double angle_right = 0;+ double angle_left = 0;+ + double length_right = 0;+ double length_left = 0;+ + bool tie_handles = false;+ + EditPoint ep;+ + for (Xml.Attr* prop = iter->properties; prop != null; prop = prop->next) {+ string attr_name = prop->name;+ string attr_content = prop->children->content;+ + if (attr_name == "x") x = double.parse (attr_content);+ if (attr_name == "y") y = double.parse (attr_content);+ + if (attr_name == "right_angle") {+ type = PointType.CURVE;+ } + + if (attr_name == "right_angle") angle_right = double.parse (attr_content);+ if (attr_name == "right_length") length_right = double.parse (attr_content);+ if (attr_name == "left_angle") angle_left = double.parse (attr_content);+ if (attr_name == "left_length") length_left = double.parse (attr_content);+ + if (attr_name == "tie_handles") tie_handles = bool.parse (attr_content);+ }+ + ep = new EditPoint (x, y);+ + ep.type = type;+ + ep.right_handle.angle = angle_right;+ ep.right_handle.length = length_right;+ + ep.left_handle.angle = angle_left;+ ep.left_handle.length = length_left;+ + ep.tie_handles = tie_handles;+ + p.add_point (ep);+ }+ + public bool restore_backup () {+ string? b = present_backup_file ();+ + if (b == null) return false;+ + return parse_file ((!)b);+ }+ + private string? present_backup_file () {+ try {+ File dir = Supplement.get_settings_directory ();+ var files = dir.enumerate_children (FILE_ATTRIBUTE_STANDARD_NAME, 0);++ // What if we have more than one backup file left?+ FileInfo? file_info;+ while ((file_info = files.next_file ()) != null) {+ FileInfo fi = (!) file_info;+ if (fi.get_name ().index_of ("current_font_") != -1) {+ File f = dir.get_child (fi.get_name ());+ backup_file = f.get_path ();+ return backup_file;+ }+ }+ } catch (GLib.Error e) {+ stderr.printf (@"Failed to load backup\n");+ stderr.printf (@"$(e.message) \n");+ }+ + return null;+ }+ + /** Delete temporary rescue files. */+ public void delete_backup () {+ if (backup_file == null) return;++ try {+ File f = File.new_for_path ((!) backup_file);+ f.delete (); + } catch (GLib.Error e) {+ stderr.printf (@"Failed to delete backup\n");+ stderr.printf (@"$(e.message) \n");+ }++ }+ + public static string to_hex (unichar ch) {+ StringBuilder s = new StringBuilder ();+ s.append ("U+");+ s.append (to_hex_code (ch));+ return s.str;+ }++ public static string to_hex_code (unichar ch) {+ StringBuilder s = new StringBuilder ();+ + uint8 a = (uint8)(ch & 0x00000F);+ uint8 b = (uint8)(ch & 0x0000F0) >> 4 * 1;+ uint8 c = (uint8)(ch & 0x000F00) >> 4 * 2;+ uint8 d = (uint8)(ch & 0x00F000) >> 4 * 3;+ uint8 e = (uint8)(ch & 0x0F0000) >> 4 * 4;+ uint8 f = (uint8)(ch & 0xF00000) >> 4 * 5;+ + if (e != 0) {+ s.append (oct_to_hex (f));+ s.append (oct_to_hex (e));+ }+ + if (c != 0) {+ s.append (oct_to_hex (d));+ s.append (oct_to_hex (c));+ }+ + s.append (oct_to_hex (b));+ s.append (oct_to_hex (a));+ + return s.str;+ }++ public static unichar to_unichar (string unicode) {+ int index = 2;+ int i = 0;+ unichar c;+ unichar rc = 0;+ bool r;++ if (unlikely (unicode.index_of ("U+") != 0)) {+ warning ("All unicode values must begin with U+");+ }+ + while (r = unicode.get_next_char (ref index, out c)) {+ rc <<= 4;+ rc += hex_to_oct (c);+ + return_val_if_fail (++i <= 6, '\0');+ }++ return rc;+ }+ + private static string oct_to_hex (uint8 o) {+ switch (o) {+ case 10: return "a";+ case 11: return "b";+ case 12: return "c";+ case 13: return "d";+ case 14: return "e";+ case 15: return "f";+ }+ + return o.to_string ();+ }++ private static uint8 hex_to_oct (unichar o) {+ switch (o) {+ case 'a': return 10;+ case 'b': return 11;+ case 'c': return 12;+ case 'd': return 13;+ case 'e': return 14;+ case 'f': return 15;+ }+ + return_val_if_fail ('0' <= o <= '9', 0);+ + return (uint8) (o - '0');+ }++}++}new file mode 100644@@ -0,0 +1,95 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;++namespace Supplement {++public abstract class FontDisplay : GLib.Object {+ + /** Queue redraw area */+ public signal void redraw_area (double x, double y, double w, double h);+ + /** Name of symbol or set of symbols in display selections like the tab panel. */+ public virtual string get_name () {+ return "";+ }+ + public virtual void draw (Allocation allocation, Context cr) {+ }+ + public virtual void selected_canvas (){+ }+ + public virtual void key_press (EventKey e){+ }+ + public virtual void key_release (EventKey e) {+ }+ + public virtual void motion_notify (EventMotion e) {+ }+ + public virtual void button_release (EventButton event) {+ }+ + public virtual void leave_notify (EventCrossing e) {+ }+ + public virtual void button_press (EventButton e) {+ }++ public virtual void zoom_in () {+ }+ + public virtual void zoom_out () {+ }+ + public virtual void zoom_max () {+ }+ + public virtual void zoom_min () {+ }+ + public virtual void reset_zoom () {+ }+ + public virtual void store_current_view () {+ warning ("store_current_view not implemented for this class.");+ }+ + public virtual void restore_last_view () {+ warning ("restore_last_view not implemented for this class.");+ }++ public virtual void next_view () {+ warning ("next_view not implemented for this class.");+ }+ + public virtual void scroll_wheel_up (Gdk.EventScroll e) {+ }+ + public virtual void scroll_wheel_down (Gdk.EventScroll e) {+ }++ public virtual void undo () {+ }+}++}new file mode 100644@@ -0,0 +1,1595 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {++class Glyph : FontDisplay {++ public const double SCALE = 1.0;++ // Background image+ GlyphBackgroundImage? background_image = null;+ bool background_image_visible = true;+ + // Glyph zoom level+ public double view_zoom = 0.01;+ public double view_offset_x = 0;+ public double view_offset_y = 0;+ List<ZoomView> zoom_list = new List<ZoomView> ();+ int zoom_list_index = 0;+ + // Paths+ public List<Path> path_list; // outline // FIXME: make private+ public Path? active_path = null;+ Path? union_path = null; // add this to active path+ bool move_edit_point = false;+ double last_move_offset_x = 0;+ double last_move_offset_y = 0;++ // Control points+ public EditPoint? active_point = null;+ public EditPoint? selected_point = null;+ public EditPoint? new_point_on_path = null;+ public EditPoint? last_added_edit_point = null;+ + // The point where edit event begun + double pointer_begin_x = 0;+ double pointer_begin_y = 0;+ + // Zoom area+ double zoom_x1 = 0;+ double zoom_y1 = 0;+ double zoom_x2 = 0;+ double zoom_y2 = 0;+ bool zoom_area_is_visible = false;+ + bool move_view = false;+ public double move_offset_x = 0;+ public double move_offset_y = 0;++ public signal void queue_draw_area (int x, int y, int w, int h);+ + public Allocation allocation;+ + public string name;++ public double left_limit;+ public double right_limit;+ + // x-height l-bearing etc.+ public List<Line> vertical_help_lines = new List<Line> ();+ public List<Line> horizontal_help_lines = new List<Line> ();+ List<Line> all_lines = new List<Line> (); // vertical, horzontal and grid lines+ bool show_help_lines = true; // grey lines+ bool xheight_lines_visible = false; // blue lines+ bool margin_boundries_visible = false; // red lines+ + public unichar unichar_code = 0;+ + bool editable = true;+ + private static List <Glyph> undo_list = new List <Glyph> ();++ public Glyph (string name, unichar unichar_code = 0) {+ this.name = name;+ this.unichar_code = unichar_code;+ + path_list.append (new Path ());+ + add_help_lines ();++ left_limit = -28 * SCALE;+ right_limit = 28 * SCALE;+ + // TODO: call redraw direcly+ queue_draw_area.connect ((x, y, w, h) => {+ redraw_area (x, y, w, h);+ });+ }++ public Glyph.no_lines (string name, unichar unichar_code = 0) {+ this.name = name;+ this.unichar_code = unichar_code;++ path_list.append (new Path ());+ }++ public void boundries (out double x1, out double y1, out double x2, out double y2) {+ if (path_list.length () == 0) {+ x1 = 0;+ y1 = 0;+ x2 = 0;+ y2 = 0;+ return;+ }++ x1 = double.MAX;+ y1 = double.MAX;+ x2 = double.MIN;+ y2 = double.MIN;+ + foreach (Path p in path_list) {+ if (p.xmin < x1) x1 = p.xmin;+ if (p.xmax > x2) x2 = p.xmax;+ if (p.ymin < y1) y1 = p.ymin;+ if (p.ymax > y2) y2 = p.ymax;+ }+ }++ /** @return centrum pixel for x coordinates. */+ public static double xc () {+ return MainWindow.get_current_glyph ().allocation.width / 2.0;+ }++ /** @return centrum pixel for y coordinates. */+ public static double yc () {+ return MainWindow.get_current_glyph ().allocation.height / 2.0;+ }++ /** @return 1/view_zoom */+ public static double ivz () {+ return 1 / MainWindow.get_current_glyph ().view_zoom;+ }+++ public void resized (Allocation o, Allocation n) {+ double a, b, c, d;+ + if (view_zoom > 1) { + a = vertical_help_lines.first ().data.get_coordinate ();+ c = horizontal_help_lines.first ().data.get_coordinate ();+ + this.allocation = n;+ + b = vertical_help_lines.first ().data.get_coordinate ();+ d = horizontal_help_lines.first ().data.get_coordinate ();+ + view_offset_x -= a - b;+ view_offset_y -= c - d;+ }+ }++ public void set_background_image (GlyphBackgroundImage bg) {+ double default_img_offset_x, default_img_offset_y;+ + default_img_offset_x = -get_width () / 2;+ default_img_offset_y = get_height () / 4;++ default_img_offset_x = 0;+ default_img_offset_y = 0;+ + bg.set_img_offset (default_img_offset_x, default_img_offset_y);+ + background_image = bg;+ + Supplement.get_current_font ().touch ();+ }+ + public GlyphBackgroundImage? get_background_image () {+ return (!) background_image;+ }+ + public override void scroll_wheel_up (Gdk.EventScroll e) {+ if (KeyBindings.has_ctrl ()) {+ zoom_in_at_point (e.x, e.y);+ } else if (KeyBindings.has_alt ()) { + view_offset_x -= 10 / view_zoom;+ } else {+ view_offset_y -= 10 / view_zoom;+ }+ + queue_draw_area (0, 0, allocation.width, allocation.height);+ }+ + public override void scroll_wheel_down (Gdk.EventScroll e) {+ if (KeyBindings.has_ctrl ()) {+ zoom_out_at_point (e.x, e.y);+ } else if (KeyBindings.has_alt ()) { + view_offset_x += 10 / view_zoom;+ } else {+ view_offset_y += 10 / view_zoom;+ }+ + queue_draw_area (0, 0, allocation.width, allocation.height);+ }+ + public void add_path (Path p) {+ path_list.append (p);+ }+ + public override void selected_canvas () {+ add_help_lines ();+ KeyBindings.singleton.set_require_modifier (false);+ }+ + private void remove_lines () {+ while (vertical_help_lines.length () != 0) {+ vertical_help_lines.remove_link (vertical_help_lines.first ());+ }++ while (horizontal_help_lines.length () != 0) {+ horizontal_help_lines.remove_link (horizontal_help_lines.first ());+ } + }+ + public void add_help_lines () {+ + remove_lines ();++ double bgt = Supplement.get_current_font ().top_limit;+ Line top_margin_line = new Line ("top margin", bgt, false);+ top_margin_line.set_color (0.7, 0, 0, 0.5);+ top_margin_line.position_updated.connect ((pos) => {+ Supplement.get_current_font ().top_limit = pos;+ });+ + double thp = Supplement.get_current_font ().top_position;+ Line top_line = new Line ("top", thp, false);+ top_line.position_updated.connect ((pos) => {+ Font f = Supplement.get_current_font ();+ f.top_position = pos;+ });+ + double xhp = Supplement.get_current_font ().xheight_position;+ Line xheight_line = new Line ("x-height", xhp, false);+ xheight_line.set_color (33 / 255.0, 68 / 255.0, 120 / 255.0, 166 / 255.0);+ xheight_line.position_updated.connect ((pos) => { + Font f = Supplement.get_current_font ();+ f.xheight_position = pos;+ });++ double xbl = Supplement.get_current_font ().base_line;+ Line base_line = new Line ("baseline", xbl, false);+ base_line.position_updated.connect ((pos) => {+ Font f = Supplement.get_current_font ();+ f.base_line = pos;+ });++ + double bp = Supplement.get_current_font ().bottom_position;+ Line bottom_line = new Line ("bottom", bp, false);+ bottom_line.set_color (33 / 255.0, 68 / 255.0, 120 / 255.0, 166 / 255.0);+ bottom_line.position_updated.connect ((pos) => {+ Supplement.get_current_font ().bottom_position = pos;+ });++ double bgb = Supplement.get_current_font ().bottom_limit;+ Line bottom_margin_line = new Line ("bottom margin", bgb, false);+ bottom_margin_line.set_color (0.7, 0, 0, 0.5);+ bottom_margin_line.position_updated.connect ((pos) => {+ Supplement.get_current_font ().bottom_limit = pos;+ });+ + Line left_line = new Line ("left", left_limit, true);+ left_line.position_updated.connect ((pos) => {+ left_limit = pos;+ });+ + Line right_line = new Line ("right", right_limit, true);+ right_line.position_updated.connect ((pos) => {+ right_limit = pos;+ });+ + // lists of help lines are sorted and lines are added only if + // they are important for a particular glyph.+ + // left to right+ add_line (left_line);+ add_line (right_line);++ bool glyph_has_top = has_top_line ();++ // top to bottom+ add_line (top_margin_line);+ top_margin_line.set_visible (margin_boundries_visible);+ + add_line (top_line);+ top_line.set_visible (glyph_has_top || xheight_lines_visible);+ + add_line (xheight_line);+ xheight_line.set_visible (!glyph_has_top || xheight_lines_visible);+ + add_line (base_line);+ + add_line (bottom_line);+ bottom_line.set_visible (CharDatabase.has_descender (unichar_code) || xheight_lines_visible);+ + add_line (bottom_margin_line);+ bottom_margin_line.set_visible (margin_boundries_visible);+ }+ + bool has_top_line () {+ return !unichar_code.islower () || CharDatabase.has_ascender (unichar_code);+ }+ + /** Show both x-height and top lines. */+ public bool get_xheight_lines_visible () {+ return xheight_lines_visible;+ }++ /** Show both x-height and top lines. */+ public void set_xheight_lines_visible (bool x) {+ xheight_lines_visible = x;+ add_help_lines ();+ }+ + public void set_margin_lines_visible (bool m) {+ margin_boundries_visible = m;+ add_help_lines ();+ }+ + public bool get_margin_lines_visible () {+ return margin_boundries_visible;+ }+ + public void remove_empty_paths () {+ foreach (var p in path_list) {+ if (p.points.length () == 0) {+ path_list.remove_all (p);+ }+ }+ }+ + /** Merge all paths. */+ public void merge () {+ bool r = false;+ + remove_empty_paths ();+ + foreach (var p in path_list) {+ foreach (var u in path_list) {+ return_if_fail (p.points.length () != 0 && u.points.length () != 0);+ + if (u != p) r = p.merge (u);+ if (r) path_list.remove_all (u);+ + }+ }+ }++ public string get_svg_data () {+ return Svg.to_svg_glyph (this, 1 / SCALE);+ }+ + public int get_height () + requires (vertical_help_lines.length () >= 2)+ {+ unowned List<Line> a = horizontal_help_lines.first ();+ unowned List<Line> b = horizontal_help_lines.last ();+ + return (int) Math.fabs (a.data.pos / SCALE - b.data.pos / SCALE); + }+ + public int get_width () {+ return (int) Math.fabs (right_limit / SCALE - left_limit / SCALE);+ }++ public unichar get_unichar () {+ return unichar_code;+ }++ public bool get_show_help_lines () {+ return show_help_lines;+ }+ + public void redraw_help_lines () {+ queue_draw_area (0, 0, allocation.width, allocation.height);+ }+ + public void set_show_help_lines (bool hl) {+ show_help_lines = hl;+ }+ + private void add_line (Line line) {+ if (line.is_vertical ()) {+ vertical_help_lines.append (line);+ line.list_item = vertical_help_lines.last ();+ } else {+ horizontal_help_lines.append (line);+ line.list_item = horizontal_help_lines.last ();+ }++ sort_help_lines ();+ + line.queue_draw_area.connect ((x, y, w, h) => {+ this.queue_draw_area (x, y, w, h);+ });+ }+ + public void sort_help_lines () {+ vertical_help_lines.sort ((a, b) => {+ return (int) (a.get_pos () - b.get_pos ());+ });+ + horizontal_help_lines.sort ((a, b) => {+ return (int) (a.get_pos () - b.get_pos ());+ });+ }+ + public override string get_name () {+ return name;+ }+ + private void help_line_event (EventMotion e) {+ double mx = e.x;+ double my = e.y;+ + if (GridTool.is_visible ()) {+ GridTool.tie (ref mx, ref my);+ }+ + foreach (Line line in get_all_help_lines ()) {+ line.event_move_to (mx, my, e.x, e.y, allocation);+ }+ }++ public override void key_press (EventKey e) { + if (e.keyval == Key.DEL) {+ if (active_path != null) {+ Path p = (!) active_path;+ close_path ();+ path_list.remove (p);+ redraw_area (0, 0, allocation.width, allocation.height);+ }+ + if (selected_point != null) {+ delete_edit_point ((!) selected_point);+ }+ + }+ }+ + /** Delete edit point from path. */+ public void delete_edit_point (EditPoint ep) {+ + foreach (Path p in path_list) {+ foreach (EditPoint e in p.points) {+ if (likely (e == ep)) {+ + p.set_new_start (e);++ if (p.is_open ()) {+ p.close ();+ p.points.remove (e);+ p.reopen ();+ } else {+ p.reopen ();+ }+ + redraw_area (0, 0, allocation.width, allocation.height);+ return;+ }+ }+ }+ + warning ("This point does not exist.");+ }+ + public override void motion_notify (EventMotion e) {+ Tool t;+ + if (KeyBindings.has_ctrl ()) {+ move_view_offset (e.x, e.y);+ return;+ }+ + t = MainWindow.get_toolbox ().get_current_tool ();+ t.move_action (t, (int) e.x, (int) e.y);+ + help_line_event (e); + }+ + public override void button_release (EventButton event) {+ bool line_moving = false;+ + if (KeyBindings.has_ctrl ()) {+ move_view = false;+ return;+ }+ + foreach (Line line in get_all_help_lines ()) {+ if (!line.set_move (false)) {+ line_moving = true;+ }+ }++ if (!line_moving) {+ Tool t = MainWindow.get_toolbox ().get_current_tool ();+ t.release_action (t, (int) event.button, (int) event.x, (int) event.y);+ }+ + move_edit_point = false;+ update_view ();+ }++ private unowned List<Line> get_all_help_lines () {+ while (all_lines.length () > 0) all_lines.delete_link (all_lines.first ());+ + all_lines.concat (vertical_help_lines.copy ());+ all_lines.concat (horizontal_help_lines.copy ());+ + if (GridTool.is_visible ()) {+ all_lines.concat (GridTool.get_vertical_lines ().copy ());+ all_lines.concat (GridTool.get_horizontal_lines ().copy ());+ }+ + return all_lines;+ }+ + public void update_view () {+ MainWindow.get_glyph_canvas ().redraw ();+ }+ + public override void leave_notify (EventCrossing e) {+ }+ + public override void button_press (EventButton e) { + pointer_begin_x = e.x;+ pointer_begin_y = e.y;+ + foreach (Line line in get_all_help_lines ()) {+ if (line.button_press (e, allocation)) {+ return;+ }+ }+ + if (KeyBindings.has_ctrl ()) {+ move_view = true;+ move_offset_x = view_offset_x;+ move_offset_y = view_offset_y;+ } else {+ Tool t = MainWindow.get_toolbox ().get_current_tool ();+ t.press_action (t, (int) e.button, (int) e.x, (int) e.y);+ }+ + MainWindow.hide_cursor ();+ }++ /** Insert new edit point for current path on the appropriate zoom+ * level.+ */+ public EditPoint add_new_edit_point (int x, int y) {+ insert_edit_point (x, y);+ move_edit_point = true;+ last_added_edit_point = ((!) active_path).get_last_point ();+ return (!) last_added_edit_point;+ }++ public EditPoint get_last_edit_point () {+ return_val_if_fail (last_added_edit_point != null, new EditPoint ());+ return (!) last_added_edit_point;+ }++ /** Move view port centrum to this coordinate. */+ public void set_center (double x, double y) {+ x -= allocation.width / 2.0;+ y -= allocation.height / 2.0;+ + view_offset_x += x / view_zoom;+ view_offset_y += y / view_zoom; + }++ public void set_zoom_from_area () {+ double x = Math.fmin (zoom_x1, zoom_x2);+ double y = Math.fmin (zoom_y1, zoom_y2);++ double w = Math.fabs (zoom_x1 - zoom_x2);+ double h = Math.fabs (zoom_y1 - zoom_y2);+ + double view_zoom_x, view_zoom_y;+ double ivz, off;++ view_offset_x += x / view_zoom;+ view_offset_y += y / view_zoom;+ + view_zoom_x = allocation.width * view_zoom / w;+ view_zoom_y = allocation.height * view_zoom / h;+ + // TODO: there is a max zoom level: probably ivz > 0.1+ + if (view_zoom_x * allocation.width < view_zoom_y * allocation.height) {+ view_zoom = view_zoom_x;+ ivz = 1 / view_zoom;++ off = (view_zoom / view_zoom_y) * allocation.height / view_zoom; + off = allocation.height/view_zoom - off;+ off /= 2;+ + view_offset_y -= off;++ } else {+ view_zoom = view_zoom_y;+ ivz = 1 / view_zoom;++ off = (view_zoom / view_zoom_x) * allocation.width / view_zoom; + off = allocation.width / view_zoom - off;+ off /= 2; ++ view_offset_x -= off;+ }+ + zoom_area_is_visible = false;+ store_current_view ();+ }+ + public void show_zoom_area (int sx, int sy, int nx, int ny) {+ double x, y, w, h;+ + set_zoom_area (sx, sy, nx, ny);+ + zoom_area_is_visible = true;+ + x = Math.fmin (zoom_x1, zoom_x2) - 50;+ y = Math.fmin (zoom_y1, zoom_y2) - 50;++ w = Math.fabs (zoom_x1 - zoom_x2) + 100;+ h = Math.fabs (zoom_y1 - zoom_y2) + 100;+ + queue_draw_area ((int)x, (int)y, (int)w, (int)h);+ }++ public void set_zoom_area (int sx, int sy, int nx, int ny) {+ zoom_x1 = sx;+ zoom_y1 = sy;+ zoom_x2 = nx;+ zoom_y2 = ny;+ }++ public static double path_coordinate_x (double x) {+ Glyph g = MainWindow.get_current_glyph ();+ double xc = Glyph.yc ();+ + return_if_fail (xc != 0);+ + x *= 1 / g.view_zoom;+ return (x - xc + g.view_offset_x);+ }++ public static int reverse_path_coordinate_x (double x) {+ Glyph g = MainWindow.get_current_glyph ();+ double xc = Glyph.xc ();+ + return_if_fail (xc != 0);+ + x *= g.view_zoom;+ return (int) (x + xc + g.view_offset_x);+ }++ public static double path_coordinate_y (double y) {+ Glyph? t = MainWindow.get_current_glyph ();+ + return_val_if_fail (t != null, 0);+ + Glyph g = (!) t;+ double yc = (g.allocation.height / 2.0);++ y *= 1 / g.view_zoom;+ + return (yc - y) - g.view_offset_y;+ }++ public static int reverse_path_coordinate_y (double y) {+ Glyph g = MainWindow.get_current_glyph ();+ double yc = (g.allocation.height / 2.0);+ y *= g.view_zoom;+ return (int) ((yc - y) + g.view_offset_y);+ }++ public static void reverse (double x, double y, out int rx, out int ry) {+ rx = Glyph.reverse_path_coordinate_x (x);+ ry = Glyph.reverse_path_coordinate_y (y); + } ++ public void move_selected_path (double x, double y) { + if (active_path == null) {+ return;+ }+ + move_selected_path_freely (path_coordinate_x (x), path_coordinate_y (y));+ }+ + public void move_selected_path_freely (double coordinate_x, double coordinate_y) + requires (active_path != null)+ {+ double dx = coordinate_x - path_coordinate_x (pointer_begin_x);+ double dy = coordinate_y - path_coordinate_y (pointer_begin_y);++ Path path = (!) active_path;++ path.move (dx - last_move_offset_x, dy - last_move_offset_y);+ + last_move_offset_x = dx;+ last_move_offset_y = dy;+ + queue_redraw_path (path);+ }+ + public void move_path_begin (double x, double y) {+ Supplement.get_current_font ().touch ();+ Path? p = null;+ + foreach (var pt in path_list) {+ if (pt.is_over (x, y)) {+ p = pt;+ }+ }+ + active_path = p;+ + last_move_offset_x = 0;+ last_move_offset_y = 0;++ move_selected_path (x, y);+ }+ + public void queue_redraw_path (Path path) {+ redraw_path (path.xmin, path.ymin, path.xmax, path.ymax);+ }+ + private void redraw_path (double xmin, double ymin, double xmax, double ymax) {+ int yc = (int)(allocation.height / 2.0);+ int xc = (int)(allocation.width / 2.0);+ + double yta = yc - ymin - view_offset_y;+ double ytb = yc - ymax - view_offset_y;++ double xta = -view_offset_x - xmin;+ double xtb = -view_offset_x - xmax;+ + // Fixa: det vore ju bra om det fanns en funktion för att rita om den+ // gamla figuren när den uppdateras.+ + // queue_draw_area ((int)xtb - 10, (int)yta - 10, (int)(xtb - xta) + 10, (int) (yta - ytb) + 10);+ + // Fixa x är inte rätt ovan+ queue_draw_area (0, 0, allocation.width, allocation.height);+ }++ public Path get_closeset_path (double x, double y) {+ double d;+ EditPoint ep = new EditPoint ();+ + Path min_point = new Path ();+ double min_distance = double.MAX;++ double xt = path_coordinate_x (x);+ double yt = path_coordinate_y (y);++ foreach (Path p in path_list) {+ if (p.is_over (xt, yt)) {+ return p;+ }+ }++ foreach (Path p in path_list) {+ if (p.points.length () == 0) continue;+ + p.get_closest_point_on_path (ep, xt, yt);+ d = Math.pow (ep.x - xt, 2) + Math.pow (ep.y - yt, 2);+ + if (d < min_distance) {+ min_distance = d;+ min_point = p;+ }++ }++ if (unlikely (min_distance == double.MAX)) {+ warning (@"No path found in path_list. Length: $(path_list.length ())");+ + if (path_list.length () > 0) {+ stderr.printf (@"p.points.length () $(path_list.first ().data.points.length ()) \n");+ }+ }+ + return min_point;+ }+ + public Path? get_active_path () {+ return active_path;+ }+ + public Path? get_path_at (int x, int y) {+ double xt, yt;+ unowned List<Path> paths;+ + if (unlikely (active_path != null)) {+ stderr.printf ("Active path already selected.\n");+ return null;+ }+ + paths = path_list;+ + xt = path_coordinate_x (x);+ yt = path_coordinate_y (y);+ + foreach (Path p in paths) {+ if (p.is_over (xt, yt)) {+ return p;+ }+ }+ + return null;+ }+ + private void insert_edit_point (double x, double y) {+ unowned List<Path> paths;+ double xt, yt;+ bool added;+ Path np;+ + if (active_path == null) {+ np = new Path ();+ active_path = np;+ path_list.append (np);+ }+ + paths = path_list;+ + xt = path_coordinate_x (x);+ yt = path_coordinate_y (y);+ + added = false;++ if (new_point_on_path != null) {+ return_if_fail (active_path != null);++ Path p = new Path ();+ EditPoint e = (!) new_point_on_path;++ return_if_fail (e.prev != null);+ + union_path = p;+ p.add (e.x, e.y);+ + paths.append (p);+ active_path = p;+ new_point_on_path = null;+ added = true;+ return;+ }+ + if (!added && union_path != null) {+ Path p = (!) union_path;+ if (p.is_open ()) {+ p.add (xt, yt);+ active_path = p;+ added = true;+ }+ }+ + if (!added) {+ foreach (Path p in paths) {+ if (p.is_open ()) {+ p.add (xt, yt);+ active_path = p;+ added = true;+ break;+ }+ }+ }++ if (!added) {+ foreach (Path p in paths) {+ if (p.is_over (xt, yt)) {+ p.add (xt, yt);+ active_path = p;+ added = true;+ queue_draw_area (0, 0, allocation.width, allocation.height);+ break;+ }+ }+ }+ + if (!added) {+ if (paths.length () > 0 && paths.last ().data.is_open ()) {+ paths.last().data.add (xt, yt);+ }+ + foreach (var p in path_list) {+ p.close ();+ }+ + np = new Path ();+ paths.append (np);+ np.add (xt, yt);+ + active_path = np;+ }+ + return_if_fail (active_path != null);+ + selected_point = ((!) active_path).get_last_point ();+ + move_selected_edit_point (x, y);+ }+ + public void move_selected_edit_point (double x, double y) { + double xc;+ double yc;+ + double ivz = 1 / view_zoom;+ + Supplement.get_current_font ().touch ();++ xc = (allocation.width / 2.0);+ yc = (allocation.height / 2.0);+ + x *= ivz;+ y *= ivz;+ + double xt = x - xc + view_offset_x;+ double yt = yc - y - view_offset_y;+ + // redraw control point+ queue_draw_area ((int)(x - 4*view_zoom), (int)(y - 4*view_zoom), (int)(x + 3*view_zoom), (int)(y + 3*view_zoom));+ + // update position of selected point+ ((!) selected_point).set_position (xt, yt);+ + if (view_zoom >= 2) {+ queue_draw_area (0, 0, allocation.width, allocation.height);+ } else {+ redraw_last_stroke (x, y);+ } + }+ + private void redraw_last_stroke (double x, double y) {+ // redraw line, if we have more than one new point on path+ double px;+ double py;+ int tw;+ int th;++ double xc = (allocation.width / 2.0);+ double yc = (allocation.height / 2.0);+ + if (active_path == null) {+ return;+ }+ + EditPoint? pt = ((!) active_path).get_second_last_point ();+ if (pt != null) {+ EditPoint p = (!) pt;+ + px = p.x + xc;+ py = p.y - xc;+ + tw = (px > x) ? (int) (px - x) : (int) (x - px);+ th = (py > y) ? (int) (py - y) : (int) (y - py);+ + if (px > x) px -= tw + 60;+ if (py > y) py -= th + 60;+ + } else {+ px = x - 60;+ py = y - 60;+ tw = 0;+ th = 0;+ }++ queue_draw_area ((int)px - 20, (int)py - 20, tw + 120, th + 120); + }+ + public Path? get_last_path () + ensures (result != null)+ {+ return_if_fail (path_list.length () > 0);+ return path_list.last ().data;+ }+ + public bool has_active_path () {+ return (active_path != null);+ }+ + /** Close all editable paths and return false if no path have boon closed. + * Paths without area (points and lines) will be deleted.+ */+ public bool close_path () {+ bool r = false;+ + foreach (var p in path_list) {+ if (p.is_editable ()) {+ r = true;+ p.close ();+ redraw_path_region (p);+ }+ }+ + editable = false;+ + active_path = null;+ union_path = null; + new_point_on_path = null;+ selected_point = null;+ + delete_invisible_paths ();+ + return r;+ }++ public void open_path () {+ editable = true;+ + foreach (var p in path_list) {+ p.set_editable (true);+ redraw_path_region (p);+ }+ }+ + public void redraw_path_region (Path p) {+ int x, y, w, h;+ + p.update_region_boundries ();+ + x = reverse_path_coordinate_x (p.xmin);+ y = reverse_path_coordinate_x (p.xmin);+ w = reverse_path_coordinate_x (p.xmax) - x;+ h = reverse_path_coordinate_x (p.ymax) - y; // FIXME: redraw path+ + queue_draw_area (x, y, w, h); + }++ /** Delete all paths without area. */+ private void delete_invisible_paths () {+ foreach (var p in path_list) {+ + if (p.points.length () < 2) {+ path_list.remove (p);+ continue;+ }+ + if (p.points.length () == 2) {+ if (p.points.first ().data.type == PointType.LINE && p.points.last ().data.type == PointType.LINE) {+ path_list.remove (p);+ continue;+ }+ }+ + }+ }++ /** lsb */+ public double get_left_marker () {++ foreach (var line in vertical_help_lines) {+ if (line.get_label () == "left") {+ return line.get_coordinate ();+ }+ }+ + warn_if_reached ();+ + return 0;+ } ++ /** rsb */+ public double get_right_marker () {+ // TODO: store this info in some smarter way.+ foreach (var line in vertical_help_lines) {+ if (line.get_label () == "right") {+ return line.get_coordinate ();+ }+ }+ + warn_if_reached ();+ + return 0;+ }++ public Line get_line (string name) { + foreach (Line line in vertical_help_lines) {+ if (likely (line.get_label () == name)) {+ return line;+ }+ }+ + foreach (Line line in horizontal_help_lines) {+ if (likely (line.get_label () == name)) {+ return line;+ }+ }+ + warning (@"No line with label $name found");+ return new Line ("Err");+ }+ + public override void zoom_in () {+ set_zoom_area (10, 10, allocation.width - 10, allocation.height - 10);+ set_zoom_from_area ();+ update_view ();+ }+ + public override void zoom_out () {+ double w = allocation.width;+ int n = (int) (10 * ((w - 10) / allocation.width));+ set_zoom_area (-n, -n, allocation.width + n, allocation.height + n);+ set_zoom_from_area ();+ update_view ();+ }+ + public override void zoom_max () {+ default_zoom ();+ }+ + public override void zoom_min () {+ double ax = 1000;+ double ay = 1000;+ double bx = -1000;+ double by = -1000;+ + int iax, iay, ibx, iby;+ + reset_zoom ();++ foreach (var p in path_list) {+ p.update_region_boundries ();+ + if (p.points.length () > 2) {+ if (p.xmin < ax) ax = p.xmin;+ if (p.ymin < ay) ay = p.ymin; + if (p.xmax > bx) bx = p.xmax;+ if (p.ymax > by) by = p.ymax;+ }+ }+ + if (ax == 1000) return; // empty page+ + // Fixa: Skrota eller ordna upp reverse och path_coordinate+ reverse (ax, ay, out iax, out iay);+ reverse (bx, by, out ibx, out iby);+ + //PenTool pen_tool = (PenTool)MainWindow.get_toolbox ().get_tool ("pen_tool");+ //pen_tool.test_click_action (1, iax, iay);+ //pen_tool.test_click_action (1, ibx, iby);++ //pen_tool.test_click_action (1, ibx, iay);+ //pen_tool.test_click_action (1, iax, iby);+ + show_zoom_area (iax - 5, iay - 5, ibx + 5, iby + 5); // set this later on button release+ set_zoom_from_area ();+ + queue_draw_area (0, 0, allocation.width, allocation.height);+ + }++ public override void store_current_view () {+ if (zoom_list_index + 1 < zoom_list.length ()) {+ unowned List<ZoomView> n = zoom_list.nth (zoom_list_index);+ while (n != zoom_list.last ()) zoom_list.delete_link (zoom_list.last ());+ }+ + zoom_list.append (new ZoomView (view_offset_x, view_offset_y, view_zoom, allocation));+ zoom_list_index = (int) zoom_list.length () - 1;+ if (zoom_list.length () > 50) zoom_list.delete_link (zoom_list.first ());+ }+ + public override void restore_last_view () + requires (zoom_list.length () > 0)+ { + if (zoom_list_index - 1 < 0 || zoom_list.length () == 0)+ return;+ + zoom_list_index--;+ + ZoomView z = zoom_list.nth (zoom_list_index).data;+ + view_offset_x = z.x;+ view_offset_y = z.y;+ view_zoom = z.zoom;+ allocation = z.allocation;+ }++ public override void next_view () + requires (zoom_list.length () > 0)+ {+ if (zoom_list_index + 1 >= zoom_list.length ())+ return;+ + zoom_list_index++;+ + ZoomView z = zoom_list.nth (zoom_list_index).data;+ + view_offset_x = z.x;+ view_offset_y = z.y;+ view_zoom = z.zoom;+ allocation = z.allocation; + }+ + public override void reset_zoom () {+ view_offset_x = 0;+ view_offset_y = 0;+ + set_zoom (1);+ + store_current_view ();+ }+ + /** Get x-height or top line. */+ public Line get_upper_line () + requires (horizontal_help_lines.length () > 2)+ {+ + if (has_top_line () || xheight_lines_visible) {+ return get_line ("top");+ }+ + return get_line ("x-height");+ }++ /** Get base line. */+ public Line get_lower_line () + requires (horizontal_help_lines.length () > 2)+ {+ return horizontal_help_lines.last ().prev.data;+ }+ + /** Set default zoom. See default_zoom. */+ public void set_default_zoom () {+ int l, t, b, r;+ + double bottom = 0;+ double top = 0;+ double left = 0;+ double right = 0;+ + unowned List<Line>? n = horizontal_help_lines;+ unowned List<Line>? v = vertical_help_lines;+ + if (unlikely (n == null)) {+ warning ("n == null");+ warning (@"Can not set default zoom for $name, help lines are not available.");+ }+ + return_if_fail (v != null);+ return_if_fail (vertical_help_lines.length () != 0);+ return_if_fail (horizontal_help_lines.length () != 0);++ reset_zoom ();+ + bottom = -get_lower_line ().get_pos ();+ top = -get_upper_line ().get_pos ();++ left = vertical_help_lines.last ().data.get_pos ();+ right = vertical_help_lines.first ().data.get_pos ();+ + l = reverse_path_coordinate_x (left);+ t = reverse_path_coordinate_y (top);+ r = reverse_path_coordinate_x (right); + b = reverse_path_coordinate_y (bottom);+ + set_zoom_area (l + 10, t - 10, r - 10, b + 10);+ set_zoom_from_area ();+ }+ + /** Set default zoom and redraw canvas. */+ public void default_zoom () {+ set_default_zoom ();+ update_view ();+ }+ + public bool is_empty () {+ bool p = path_list.length () == 0 || path_list.first ().data.points.length () == 0;+ return p && background_image == null;+ }+ + private void set_zoom (double z)+ requires (z > 0)+ { + view_zoom = z;+ }++ public void set_background_visible (bool visibility) {+ background_image_visible = visibility;+ }+ + public bool get_background_visible () {+ return background_image_visible;+ }+ + private void draw_path (Context cr) {+ double left, baseline;+ + cr.save ();+ + baseline = get_line ("baseline").pos;+ left = get_line ("left").pos;+ + if (!editable) {+ Svg.draw_svg_path (cr, get_svg_data (), Glyph.xc () + left, Glyph.yc () + baseline, SCALE);+ }+ + foreach (unowned Path p in path_list) {+ p.draw_edit_points (cr, allocation, view_zoom);+ }++ if (new_point_on_path != null) {+ Path.draw_edit_point_center ((!) new_point_on_path, cr);+ } ++ cr.restore ();+ }+ + private void draw_zoom_area(Context cr) {+ cr.save ();+ cr.set_line_width (2.0);+ cr.set_source_rgba (1, 0, 0, 0.3);+ cr.rectangle (Math.fmin (zoom_x1, zoom_x2), Math.fmin (zoom_y1, zoom_y2), Math.fabs (zoom_x1 - zoom_x2), Math.fabs (zoom_y1 - zoom_y2));+ cr.fill_preserve ();+ cr.restore ();+ }++ private void draw_background_color (Context cr, double opacity) {+ cr.save ();+ cr.rectangle (0, 0, allocation.width, allocation.height);+ cr.set_line_width (0);+ cr.set_source_rgba (1, 1, 1, opacity);+ cr.fill ();+ cr.stroke ();+ cr.restore ();+ }+ + public void draw_full_glyph (Allocation alloc, Context cr, double zoom) {+ double vx = view_offset_x;+ double vy = view_offset_y;+ double z = view_zoom;+ double lsb, rsb;+ + this.allocation = alloc;+ view_zoom = zoom;++ view_offset_x = -((this.allocation.width / view_zoom) / 2.0) + (alloc.width / 2.0);+ view_offset_y = -((this.allocation.height / view_zoom) / 2.0) + (alloc.height / 2.0); + + // center+ lsb = get_left_marker ();+ rsb = get_right_marker ();++ view_offset_x += lsb;++ cr.save ();+ draw (alloc, cr);+ cr.restore ();+ + view_offset_x = vx;+ view_offset_y = vy;+ view_zoom = z;+ }+ + private void draw_help_lines (Context cr) {+ + // lines + foreach (Line line in get_all_help_lines ()) {+ cr.save ();+ line.draw (cr, allocation);+ cr.restore ();+ }+ + }+ + public override void draw (Allocation allocation, Context cr) {+ Tool tool;+ + this.allocation = allocation;++ ImageSurface ims = new ImageSurface (Format.ARGB32, allocation.width, allocation.height);+ Context cms = new Context (ims);+ + ImageSurface ps = new ImageSurface (Format.ARGB32, allocation.width, allocation.height);+ Context cmp = new Context (ps);++ cr.save ();+ draw_background_color (cr, 1);+ cr.restore ();++ if (show_help_lines) {+ cmp.save ();+ cmp.scale (view_zoom, view_zoom);+ cmp.translate (-view_offset_x, -view_offset_y);+ draw_help_lines (cmp);+ cmp.restore ();+ }++ if (background_image != null && background_image_visible) {+ cmp.save ();+ ((!)background_image).draw (cr, allocation, view_offset_x, view_offset_y, view_zoom);+ cmp.restore ();+ }+ + cmp.save ();+ cmp.scale (view_zoom, view_zoom);+ cmp.translate (-view_offset_x, -view_offset_y);+ draw_path (cmp);+ cmp.restore ();+ + cmp.save (); + tool = MainWindow.get_toolbox ().get_current_tool ();+ tool.draw_action (tool, cmp, this);+ cmp.restore ();+ + // detta kanske är en del av verktyget+ if (zoom_area_is_visible) {+ cmp.save ();+ draw_zoom_area (cmp);+ cmp.restore ();+ }+ + cr.save ();+ cr.set_source_surface (ps, 0, 0);+ cr.paint ();+ cr.restore ();+ } + + private void add_layer (ImageSurface path_image, ImageSurface background_image, Context cr) {+ cr.save ();+ unowned uchar[] pb = background_image.get_data ();+ unowned uchar[] pp = path_image.get_data ();+ int len;++ int bw = background_image.get_width ();+ int bh = background_image.get_height ();++ int pw = path_image.get_width ();+ int ph = path_image.get_height ();+ + ImageSurface cs = new ImageSurface (Format.ARGB32, pw, ph);+ + unowned uchar[] img = cs.get_data ();+ + len = pw * ph * 4;+ int i = 0;+ while (i < len) {+ img[i++] = 0;+ }++ len = pw * ph * 4;+ int pi = 0;+ int bi = 0;+ + while (true) {+ + for (int c = 0; c < bw && c < pw; c += 4) {+ switch (pp[c+3]) {+ case 255:+ img[c + pi] = pb[c + bi];+ img[c+1 + pi] = pb[c+1 + bi];+ img[c+2 + pi] = pb[c+2 + bi];+ img[c+3 + pi] = 255;+ break;+ + default:+ img[c + pi] = pp[c + bi];+ img[c+1 + pi] = pp[c+1 + bi];+ img[c+2 + pi] = pp[c+2 + bi];+ img[c+3 + pi] = 255;+ break;+ }+ }+ + bi += bw;+ pi += pw;+ + if (bi >= 4 * bw * bh) {+ break;+ }+ + if (pi >= 4 * pw * ph) {+ break;+ }+ + }++ cr.set_source_surface (cs, 0, 0);+ + cr.paint ();+ cr.restore ();+ }++ private void zoom_in_at_point (double x, double y) {+ int n = -10;+ zoom_at_point (x, y, n);+ }+ + private void zoom_out_at_point (double x, double y) {+ int n = (int) (10.0 * ((allocation.width - 10.0) / allocation.width));+ zoom_at_point (x, y, n);+ }+ + /** Zoom in @param n pixels. */+ private void zoom_at_point (double x, double y, int n) {+ double w = allocation.width;+ double h = allocation.height;+ + double rx = Math.fabs (w / 2 - x) / (w / 2);+ double ry = Math.fabs (h / 2 - y) / (h / 2);+ + int xd = (x < w / 2) ? (int) (n * rx) : (int) (-n * rx);+ int yd = (y < h / 2) ? (int) (n * ry) : (int) (-n * ry);+ + show_zoom_area (-n + xd, -n + yd, allocation.width + n + xd, allocation.height + n + yd);+ set_zoom_from_area ();+ }+ + private void move_view_offset (double x, double y) {+ if (move_view) {+ view_offset_x = move_offset_x + (pointer_begin_x - x) * (1/view_zoom);+ view_offset_y = move_offset_y + (pointer_begin_y - y) * (1/view_zoom);+ redraw_area (0, 0, allocation.width, allocation.height);+ }+ }++ public void store_undo_state () {+ Glyph g = copy ();+ undo_list.append (g); + }++ public Glyph copy () {+ Glyph g = new Glyph.no_lines (name, unichar_code);+ + g.left_limit = left_limit;+ g.right_limit = right_limit;+ + g.remove_lines ();+ + foreach (Line line in get_all_help_lines ()) {+ g.add_line (line.copy ());+ }++ foreach (Path p in path_list) {+ g.add_path (p.copy ());+ }+ + g.active_path = active_path;+ + return g;+ }++ public override void undo () {+ Glyph g;+ + if (undo_list.length () == 0) {+ return;+ }+ + g = undo_list.last ().data;+ + while (path_list.length () > 0) {+ path_list.remove_link (path_list.last ());+ }+ + foreach (Path p in g.path_list) {+ add_path (p);+ }++ remove_lines ();+ foreach (Line line in g.get_all_help_lines ()) {+ add_line (line.copy ());+ }+ + active_path = g.active_path;+ + undo_list.remove_link (undo_list.last ());+ }+ +}++}new file mode 100644@@ -0,0 +1,714 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {+ +class GlyphBackgroundImage { + public double img_offset_x = 0;+ public double img_offset_y = 0;+ public double img_scale_x = 1;+ public double img_scale_y = 1;+ public double img_rotation = 0;+ + public int active_handle = -1;+ public int selected_handle = -1;+ + public bool scaled = false;+ + private ImageSurface? background_image = null;+ private ImageSurface? original_image = null;+ + private string path;++ private double contrast = 1.0;+ private bool desaturate = false;+ private double threshold = 1.0;+ + private bool update_sheduled = false; + private bool background_image_is_processing = false;++ public signal void updated ();+ + public GlyphBackgroundImage (string fn = "") {+ path = fn;+ }+ + public ImageSurface get_img () {+ if (path.index_of (".png") == -1) {+ create_png ();+ }+ + if (background_image == null) {+ background_image = new ImageSurface.from_png (path);+ original_image = new ImageSurface.from_png (path);+ }+ + return (!) background_image;+ }+ + private void create_png () {+ Gtk.Image img = new Gtk.Image.from_file (path);+ Pixbuf buf = img.pixbuf;+ Context ct;+ Bitmap mask;+ Pixmap pixmap;+ string fn = @"$path.png";+ Font font = Supplement.get_current_font ();+ File folder = font.get_backgrounds_folder ();+ File original = File.new_for_path (fn);+ File png_image = folder.get_child (@"full_$((!)original.get_basename ())");+ + if (png_image.query_exists ()) {+ path = (!) png_image.get_path ();+ return;+ }+ + pixbuf_render_pixmap_and_mask (buf, out pixmap, out mask, 1);+ ct = cairo_create (pixmap);+ ct.get_target ().write_to_png ((!) png_image.get_path ());++ path = (!) png_image.get_path ();+ }+ + public File get_thumbnail_file () {+ Font font = Supplement.get_current_font ();+ File folder = font.get_backgrounds_folder ();+ File full = File.new_for_path (path);+ File thumbnail = folder.get_child (@"thumbnail_$((!)full.get_basename ())");+ + if (!thumbnail.query_exists ()) {+ create_thumbnail (get_img (), thumbnail);+ }+ + return thumbnail;+ }++ private void create_thumbnail (ImageSurface source, File thumbnail) {+ Context c;+ Surface s;+ double zoom;+ ImageSurface img = source;+ + zoom = Math.fmin (80.0 / img.get_width(), 80.0 / img.get_height());++ s = new Surface.similar (img, img.get_content (), 80, 80);+ c = new Context (s);++ c.set_source_rgba (1, 1, 1, 1);+ c.rectangle (0, 0, 80, 80);+ c.fill ();+ + c.scale (zoom, zoom);+ c.set_source_surface (img, 0, 0);+ c.paint ();+ + c.get_target ().write_to_png ((!) thumbnail.get_path ());+ }+ + public double get_current_width () {+ return get_img ().get_width () * img_scale_x;+ }++ public double get_current_height () {+ return get_img ().get_height () * img_scale_y;+ }+ + public void set_img_rotation (double angle) {+ img_rotation = angle;+ }+ + public double get_img_rotation () {+ return img_rotation;+ }+ + public void set_img_rotation_from_coordinate (double x, double y) {+ double bcx, bcy;+ double a, b, c, length;++ img_center (out bcx, out bcy);++ a = bcx - x;+ b = bcy - y;+ c = a * a + b * b;+ + if (c == 0) {+ return;+ }+ + length = sqrt (fabs (c)); + + if (c < 0) {+ length = -length;+ }+ + img_rotation = (y < bcy) ? acos (a / length) + PI : -acos (a / length) + PI;+ }+ + public void set_img_scale (double xs, double ys) {+ img_scale_x = xs;+ img_scale_y = ys;+ }++ public void set_img_offset (double x, double y) {+ img_offset_x = x;+ img_offset_y = y;+ }++ public string get_path () {+ return path;+ }++ public void scale_image_proportional_to_boundries (Glyph g) {+ double w, h;+ + w = g.get_width ();+ h = g.get_height ();+ + g.reset_zoom ();++ img_scale_y = h / get_img ().get_height ();+ + img_offset_x = g.get_line ("left").pos; + img_offset_y = g.vertical_help_lines.first ().data.pos;++ }++ public void reset_scale (Glyph g) {+ double w, h;++ w = g.get_width ();+ h = g.get_height ();++ img_scale_x = 1; + img_scale_y = 1;+ + img_offset_x = g.get_line ("left").pos;+ img_offset_y = g.get_line ("top").pos; + }++ public void draw (Context cr, Allocation allocation, double view_offset_x, double view_offset_y, double view_zoom) {+ double x, y;+ int h, w;+ + Surface s;+ Context c;++ if (unlikely (get_img ().status () != Cairo.Status.SUCCESS)) {+ stderr.printf (@"Background image is invalid. (\"$path\")\n");+ MainWindow.get_current_glyph ().set_background_visible (false);+ return;+ }+ + x = -view_offset_x + allocation.width / 2.0; // center for glyph+ y = -view_offset_y + allocation.height / 2.0;+ + x += img_offset_x;+ y += img_offset_y;+ + cr.save ();+ + h = (int) get_img ().get_height ();+ w = (int) get_img ().get_width ();+ + if (w < allocation.width) {+ w = allocation.width;+ }+ + if (h < allocation.height) {+ h = allocation.height;+ }+ + s = new Surface.similar (get_img (), get_img ().get_content (), w, h);+ c = new Context (s);++ c.save ();+ c.set_source_rgba (1, 1, 1, 1);+ c.rectangle (0, 0, w, h);+ c.fill ();+ c.restore ();+ + c.rotate (img_rotation);+ c.scale (view_zoom * img_scale_x, view_zoom * img_scale_y);+ c.set_source_surface (get_img (), 0, 0);+ c.paint ();+ + cr.save ();+ cr.set_source_rgba (1, 1, 1, 1);+ cr.rectangle (0, 0, w, h);+ cr.fill ();+ cr.restore ();+ + cr.set_source_surface (s, x * view_zoom, y * view_zoom);+ cr.paint ();+ + cr.restore ();+ }+ + public void handler_release (double nx, double ny) { + selected_handle = 0;+ handler_move (nx, ny);+ }+ + public void handler_press (double nx, double ny) { + if (is_over_rotate (nx, ny)) {+ selected_handle = 2;+ } else if (is_over_resize (nx, ny)) {+ selected_handle = 1;+ } else {+ selected_handle = 0;+ } + }++ void img_center (out double x, out double y) {+ double a, b;+ Glyph g = MainWindow.get_current_glyph ();+ + x = img_offset_x - g.view_offset_x + g.allocation.width / 2.0;+ y = img_offset_y - g.view_offset_y + g.allocation.height / 2.0;+ + y *= g.view_zoom;+ x *= g.view_zoom;+ + a = get_img ().get_width () / 2.0 * img_scale_x;+ b = get_img ().get_height () / 2.0 * img_scale_y;++ a *= g.view_zoom;+ b *= g.view_zoom;+ + x += a;+ y += b;+ }++ bool is_over_rotate (double nx, double ny) {+ double x, y, d;++ img_center (out x, out y);++ x += cos (img_rotation) * 75;+ y += sin (img_rotation) * 75;++ d = sqrt (pow (x - nx, 2) + pow (y - ny, 2));+ + return d < 10;+ }++ bool is_over_resize (double nx, double ny) {+ Glyph g = MainWindow.get_current_glyph ();+ double x, y, cx, cy, size, w, h;+ bool inx, iny;++ size = 12 * g.view_zoom;+ + x = img_offset_x - g.view_offset_x;+ y = img_offset_y - g.view_offset_y + get_img ().get_height () * img_scale_y;++ cx = g.allocation.width / 2.0;+ cy = g.allocation.height / 2.0;+ + w = 0;+ h = get_img ().get_height () * img_scale_y;+ + x = img_offset_x + w - g.view_offset_x + g.allocation.width / 2.0;+ y = img_offset_y + h - g.view_offset_y + g.allocation.height / 2.0;+ + y *= g.view_zoom;+ x *= g.view_zoom;+ + inx = x - size <= nx <= x + size;+ iny = y - size <= ny <= y + size;+ + return inx && iny;+ }++ public void handler_move (double nx, double ny) {+ int prev_handle = active_handle;++ if (is_over_rotate (nx, ny)) {+ active_handle = 2; + } else if (is_over_resize (nx, ny)) {+ active_handle = 1;+ } else {+ active_handle = 0;+ }+ + if (prev_handle != active_handle) {+ MainWindow.get_glyph_canvas ().redraw ();+ }+ }+ + public void draw_handle (Context cr, Glyph g) {+ draw_resize_handle (cr, g);+ draw_rotate_handle (cr, g);+ }+ + public void draw_rotate_handle (Context cr, Glyph g) {+ double x, y, a, b, hx, hy, x1, y1, x2, y2;+ + double ivz = 1.0 / (g.view_zoom);+ + cr.save ();++ if (selected_handle == 2) cr.set_source_rgba (1, 0, 0.3, 1);+ else if (active_handle == 2) cr.set_source_rgba (0, 0, 0.3, 1);+ else cr.set_source_rgba (0.7, 0.7, 0.8, 1);+ + cr.set_line_width (ivz);++ a = get_img ().get_width () / 2.0 * img_scale_x;+ b = get_img ().get_height () / 2.0 * img_scale_y;++ cr.scale (g.view_zoom, g.view_zoom);+ + x = img_offset_x + a - g.view_offset_x + g.allocation.width / 2.0;+ y = img_offset_y + b - g.view_offset_y + g.allocation.height / 2.0;+ + hx = cos (img_rotation) * 75;+ hy = sin (img_rotation) * 75;+ + x1 = x + 2.5 * ivz;+ y1 = y + 2.5 * ivz;+ + x2 = x + 2.5 * ivz + hx * ivz;+ y2 = y + 2.5 * ivz + hy * ivz;+ + cr.move_to (x1, y1);+ cr.line_to (x2, y2);+ cr.stroke ();+ + cr.rectangle (x, y, 5 * ivz, 5 * ivz);+ cr.rectangle (x2 - 5 * ivz, y2 - 5 * ivz, 10 * ivz, 10 * ivz); // Fixa: do arc instead++ cr.fill ();++ cr.restore ();+ }+ + public void draw_resize_handle (Context cr, Glyph g) {+ double x, y;+ + double w = 0;+ double h = get_img ().get_height () * img_scale_y;++ cr.save ();+ + cr.scale (g.view_zoom, g.view_zoom);+ + x = img_offset_x + w - g.view_offset_x + g.allocation.width / 2.0;+ y = img_offset_y + h - g.view_offset_y + g.allocation.height / 2.0;+ + draw_handle_triangle (x - 1, y - 1, cr, g, 6);+ + cr.restore ();+ }++ void draw_handle_triangle (double x, double y, Context cr, Glyph g, int direction, double s = 1) + requires (0 < direction < 8)+ {+ double ivz = 1.0 / (g.view_zoom);+ double size;+ + cr.save ();+ cr.set_line_width (ivz);+ + if (selected_handle == 1) cr.set_source_rgba (1, 0, 0.3, 1);+ else if (active_handle == 1) cr.set_source_rgba (0, 0, 0.3, 1);+ else cr.set_source_rgba (0.7, 0.7, 0.8, 1); ++ size = (8 * ivz) * s;+ + cr.scale (1, 1);+ cr.new_path ();+ + // up + left+ if (direction == 1) {+ cr.move_to (x - size, y - size);+ cr.line_to (x + size, y - size);+ cr.line_to (x - size, y + size); + }+ + if (direction == 6) {+ cr.move_to (x + size, y + size); + cr.line_to (x - size, y + size);+ cr.line_to (x - size, y - size); + }+ + cr.close_path();+ cr.fill ();++ cr.restore ();+ + }++ private static uchar ftu (double c) {+ if (c < 0) return 0;+ if (c > 255) return 255;+ return (uchar) c;+ }++ private static ImageSurface? process_background_colors (ImageSurface img, double contrast, bool desaturate, double threshold) {+ int i;+ unowned uchar[]? pix_buff_t;+ unowned uchar[] pix_buff;+ int len;+ uchar[] contrast_img;+ ImageSurface contrast_surface;+ double c, thres;+ uchar nc;++ int w = img.get_width();+ int h = img.get_height();++ if (unlikely (img.status () != Cairo.Status.SUCCESS)) {+ warning ("Err");+ return null;+ }+ + len = 4 * w * h;+ + pix_buff_t = img.get_data ();+ return_if_fail (pix_buff_t != null);+ pix_buff = pix_buff_t;+ + contrast_img = new uchar[len];++ pix_buff_t = contrast_img;+ return_if_fail (pix_buff_t != null);+ + contrast += 70;+ contrast /= 71;+ + if (desaturate) {+ i = 0;+ + while (i < len) {+ c = (pix_buff[i] + pix_buff[i+1] + pix_buff[i+2]) / 3.0;+ nc = ftu (Math.pow (c, contrast));++ contrast_img[i] = nc;+ contrast_img[i+1] = nc; + contrast_img[i+2] = nc;+ contrast_img[i+3] = 255;+ + i += 4;+ }+ } else {+ i = 0;+ while (i < len) {+ contrast_img[i] = ftu (Math.pow (pix_buff[i], contrast) - Math.pow (100, contrast) + 100);+ contrast_img[i+1] = ftu (Math.pow (pix_buff[i+1], contrast) - Math.pow (100, contrast) + 100);+ contrast_img[i+2] = ftu (Math.pow (pix_buff[i+2], contrast) - Math.pow (100, contrast) + 100);+ contrast_img[i+3] = 255;+ + i += 4;+ }+ }++ if (true) {+ thres = (threshold - 0.5) * 255;+ + i = 0;+ while (i < len) {+ c = (pix_buff[i] + pix_buff[i+1] + pix_buff[i+2]) / 3.0;+ nc = (c > thres) ? 255 : 0;++ contrast_img[i] = nc;+ contrast_img[i+1] = nc; + contrast_img[i+2] = nc;+ contrast_img[i+3] = 255;+ + i += 4;+ }+ }++ contrast_surface = new ImageSurface.for_data (contrast_img, img.get_format(), w, h, img.get_stride());++ if (unlikely (contrast_surface.status () != Cairo.Status.SUCCESS)) {+ warning ("Err");+ return null;+ }+ + contrast_surface.mark_dirty ();+ + return contrast_surface; + }++ async ImageSurface? do_process_background_colors () {+ SourceFunc callback = do_process_background_colors.callback;+ ImageSurface? output = null;+ double contrast = get_contrast ();+ bool desaturate = get_desaturate_background ();+ double threshold = this.threshold;++ if (original_image == null) {+ return null;+ }+ + // Fixa: Add multi threading+ //ThreadFunc<void*> run = () => {+ output = process_background_colors ((!)original_image, contrast, desaturate, threshold);+ Idle.add ((owned) callback);+ //return null;+ //};+ + try {+ //Thread.create<void*> (run, false);+ yield;+ } catch (ThreadError e) {+ stderr.printf ("Thread error: %s\n", e.message);+ }++ return output;+ }++ public void update_background (double contrast, bool desaturate) {+ background_image_is_processing = true;+ + do_process_background_colors.begin ((obj, res) => {+ ImageSurface? img;++ img = do_process_background_colors.end (res);+ + if (img != null) {+ background_image = (!) img;+ updated ();+ } else {+ stderr.printf ("Error: No background image"); + }+ + // update image if new value was set while background task was running + if (get_contrast () != contrast || get_desaturate_background () != desaturate) {++ do_process_background_colors.begin ((obj, res) => {+ img = do_process_background_colors.end (res);+ if (likely (img != null)) {+ background_image = (!) img;+ updated ();+ } else {+ stderr.printf ("Error: No background image"); + }+ });+ }++ background_image_is_processing = false;+ updated ();+ + });+ }++ public double get_contrast () {+ double bc;+ lock (contrast) {+ bc = contrast;+ }+ return bc;+ }+ + public bool get_desaturate_background () {+ bool bw;+ lock (desaturate) {+ bw = desaturate;+ }+ return bw;+ }+ + public void set_desaturate_background (bool bw) {+ lock (desaturate) {+ desaturate = bw;+ }+ + if (background_image_is_processing) {+ return;+ }+ + background_image_is_processing = true;+ + update_background (get_contrast (), bw);+ }+ + public void set_threshold (double t) {+ threshold = t;+ }+ + public Path auto_trace () {+ ImageSurface img;+ int len, w, h, i;+ unowned uchar[] pix_buff;+ Path p = new Path ();+ double x, y;++ if (background_image == null) {+ return p;+ }++ img = (!) background_image;++ w = img.get_width();+ h = img.get_height();++ if (unlikely (img.status () != Cairo.Status.SUCCESS)) {+ warning ("Err");+ return p;+ }+ + len = 4 * w * h;++ pix_buff = img.get_data ();++ i = 0;+ while (i < len - 4) {+ if (is_edge (pix_buff[i], pix_buff[i + 4])) {+ y = i / w;+ x = i - y;+ + p.add (x, y);+ + if (p.points.length () > 10) {+ return p;+ }+ }+ + i += 4;+ }+ + return p;+ }+ + private bool is_edge (uchar a, uchar b) {+ return (a == 255 && b == 0);+ }+ + public void set_contrast (double contrast) {+ lock (this.contrast) {+ this.contrast = contrast;+ }+ + if (background_image_is_processing) {+ return;+ }+ + background_image_is_processing = true;+ + update_background (contrast, get_desaturate_background ());+ }+}+ +}new file mode 100644@@ -0,0 +1,182 @@+/* + Copyright (C) 2012 Johan Mattsson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +using Cairo; +using Gtk; +using Gdk; +using Math; + +namespace Supplement { + +class GlyphCanvas : DrawingArea { + + public Glyph current_glyph = new Glyph (""); + FontDisplay current_display; + + Allocation alloc; + + public GlyphCanvas () { + set_size_request (300, 50); + + alloc.width = 0; + alloc.height = 0; + + current_display = current_glyph; + + expose_event.connect ((t, e)=> { + + Allocation allocation; + get_allocation (out allocation); + + if (unlikely (allocation != alloc && alloc.width != 0)) { + // Set size of glyph widget to an even number and notify + // set new allocation for glyph + bool ug = false; + + if (allocation.height % 2 != 0) { + MainWindow.toggle_expanded_margin_bottom (); + ug = true; + } + + if (allocation.width % 2 != 0) { + MainWindow.toggle_expanded_margin_right (); + ug = true; + } + + if (ug) { + redraw_area (1, 1, 2, 2); + } else if (unlikely (allocation.width % 2 != 0 || allocation.height % 2 != 0)) { + warning (@"\nGlyph canvas is not divisible by two.\nWidth: $(allocation.width)\nHeight: $(allocation.height)"); + } + + current_glyph.resized (alloc, allocation); + Preferences.update_window_size (); + } + + alloc = allocation; + + Context cw = cairo_create (get_window()); + + Surface s = new Surface.similar (cw.get_target (), Cairo.Content.COLOR_ALPHA, allocation.width, allocation.height); + Context c = new Context (s); + + current_display.draw (allocation, c); + + cw.save (); + cw.set_source_surface (c.get_target (), 0, 0); + cw.paint (); + cw.restore (); + + return true; + }); + + add_events (EventMask.BUTTON_PRESS_MASK | EventMask.BUTTON_RELEASE_MASK | EventMask.POINTER_MOTION_MASK | EventMask.LEAVE_NOTIFY_MASK | EventMask.SCROLL_MASK); + + leave_notify_event.connect ((t, e)=> { + current_display.leave_notify (e); + return true; + }); + + button_press_event.connect ((t, e)=> { + current_display.button_press (e); + return true; + }); + + button_release_event.connect ((t, e)=> { + current_display.button_release (e); + return true; + }); + + motion_notify_event.connect ((t, e)=> { + current_display.motion_notify (e); + return true; + }); + + key_press_event.connect ((t, e)=> { + key_press (e); + return true; + }); + + key_release_event.connect ((t, e)=> { + key_release (e); + return true; + }); + + scroll_event.connect ((t, e)=> { + if (e.direction == Gdk.ScrollDirection.UP) { + current_display.scroll_wheel_up (e); + } else if (e.direction == Gdk.ScrollDirection.DOWN) { + current_display.scroll_wheel_down (e); + } + + return true; + }); + + } + + public void key_release (EventKey e) { + current_display.key_release (e); + } + + public void key_press (EventKey e) { + current_display.key_press (e); + } + + public void set_current_glyph (FontDisplay fd) { + if (fd is Glyph) { + Allocation allocation; + get_allocation (out allocation); + + Glyph g = (Glyph) fd; + + g.allocation = allocation; + + current_glyph = g; + } + + current_display = fd; + + fd.selected_canvas (); + + fd.redraw_area.connect ((x, y, w, h) => { + queue_draw_area ((int)x, (int)y, (int)w, (int)h); + }); + + redraw (); + } + + public Glyph get_current_glyph () { + return current_glyph; + } + + public FontDisplay get_current_display () { + return current_display; + } + + // Deprecated + private void redraw_area (int x, int y, int w, int h) { + queue_draw_area (x, y, w, h); + } + + public void redraw () { + Allocation allocation; + get_allocation(out allocation); + queue_draw_area (0, 0, allocation.width, allocation.height); + } +} + +} new file mode 100644@@ -0,0 +1,49 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {++class GlyphCollection : GLib.Object {+ VersionList versions;+ + public GlyphCollection (Glyph? current = null) {+ versions = new VersionList (current);+ }+ + public VersionList get_version_list () {+ return versions;+ }+ + public Glyph get_current () {+ return versions.get_current ();+ }+ + public void insert_glyph (Glyph g, bool selected) {+ versions.add_glyph (g, selected);+ }+ + public string get_name () {+ return get_current ().get_name ();+ }+}+ +}new file mode 100644@@ -0,0 +1,224 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class GlyphRange {+ + List<UniRange> ranges;+ + List<unichar> index_start;+ + unichar len = 0;+ + public bool is_fill_unicode = false;+ + public GlyphRange () {+ }+ + public void use_default_range () {+ add_range ('a', 'z');++ add_single ('å');+ add_single ('ä');+ add_single ('ö');++ add_range ('A', 'Z');++ add_single ('Å');+ add_single ('Ä');+ add_single ('Ö');+ + add_range ('0', '9');+ + add_single (' '); // TODO: add all spaces here.+ + add_single ('.');+ add_single ('?');+ + add_single (',');+ + add_single ('’');++ add_range ('“', '”');++ add_single ('&');+ + add_range (':', ';');+ + add_single ('/'); + + add_range ('!', '/');+ + add_single ('-');+ add_range ('‐', '—');+ add_range ('<', '@');+ add_range ('(', ')');+ }+ + public void use_full_unicode_range () {+ is_fill_unicode = true;+ add_range ('\0', (unichar) 0xA868FE00);+ }+ + // Todo: complete localized alphabetical sort åäö is the right order for example.+ public void sort () {+ ranges.sort ((a, b) => {+ bool r = a.start > b.start;+ + return_if_fail (a.start != b.start);+ + return (r) ? 1 : -1;+ });+ }+ + public void add_single (unichar c) {+ add_range (c, c);+ }+ + public unichar get_length () {+ return len;+ }+ + public void add_range (unichar start, unichar stop) {+ if (unique (start, stop)) {+ insert_range(start, stop);+ } else {+ + unichar b = start;+ unichar s = b;+ while (b < stop) {+ if (unique (b, b)) {+ b++;+ } else {+ if (s != b) {+ insert_range(s, b - 1);+ }+ + b++;+ s = b;+ }+ }+ }+ + }+ + public unichar get_unichar (unichar index) {+ return get_char (index).get_char (0);+ }+ + public string get_char (unichar index) {+ int i = 0;+ unichar next = 0;+ foreach (unichar s in index_start) {+ StringBuilder sbt = new StringBuilder ();+ + if (!s.validate () && s >= index) {+ + sbt.append_unichar (s);+ break;+ } else if (s >= index) {+ sbt.append_unichar (s);+ break;+ }+ + next = s;+ i++;+ }++ if (unlikely(index == 0)) {+ return "\0".dup();+ }++ if (!unlikely(0 <= i < ranges.length ())) {+ return "\0".dup();+ }++ UniRange u = ranges.nth_data (i);+ StringBuilder sb = new StringBuilder ();+ unichar c = u.get_char (index - next - 1);+ + sb.append_unichar (c);++ return sb.str;+ }+ + public unichar length () {+ return len;+ }++ private bool unique (unichar start, unichar stop) {+ foreach (UniRange u in ranges) {+ + if (inside (start, u.start, u.stop)) return false;+ if (inside (stop, u.start, u.stop)) return false;+ if (inside (u.start, start, stop)) return false;+ if (inside (u.stop, start, stop)) return false;++ }++ return true;+ }++ private bool inside (unichar start, unichar u_start, unichar u_stop) {+ return (u_start <= start <= u_stop);+ }+ + private void insert_range (unichar start, unichar stop) {+ if (unlikely (start > stop)) {+ warning ("start > stop");+ return;+ }+ + UniRange ur = new UniRange (start, stop);+ len += ur.length ();+ index_start.append (len);+ ranges.append (ur);+ }++class UniRange : GLib.Object {+ + public unichar start;+ public unichar stop;+ + public UniRange (unichar start, unichar stop) {+ this.start = start;+ this.stop = stop;+ }+ + public unichar length () {+ return stop - start + 1;+ }++ public unichar get_char (unichar index) {+ unichar result = start + index;+ + if (unlikely (index < 0)) {+ warning ("Index is negative in UniRange.");+ }+ + if (unlikely (!(start <= result <= stop))) {+ warning ("Index out of range in UniRange (%u <= %u <= %u) (index: %u)\n", start, result, stop, index);+ }+ + return result;+ }+}+ +}++}new file mode 100644@@ -0,0 +1,244 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Math;++namespace Supplement {++class GridTool : Tool {+ + static List <Line> horizontal;+ static List <Line> vertical;+ + static bool visible = false;+ + public static double size_x;+ public static double size_y;+ + public static List <SpinButton> sizes;+ + public GridTool (string n) {+ base (n, "show grid", 'g', NONE);+ + horizontal = new List <Line> ();+ vertical = new List <Line> ();+ + sizes = new List <SpinButton> ();+ + size_x = 2;+ size_y = 2;+ + updata_lines ();+ + select_action.connect((self) => {+ visible = !visible;+ updata_lines ();+ MainWindow.get_glyph_canvas ().redraw ();+ + self.set_selected (visible);+ + Toolbox tb = MainWindow.get_toolbox ();+ Tool t = tb.get_tool ("help_lines");+ + if (visible && !t.is_selected ()) {+ MainWindow.get_toolbox ().select_tool (t);+ }+ + });+ + press_action.connect((self, b, x, y) => {+ });++ release_action.connect((self, b, x, y) => {+ });+ + move_action.connect ((self, x, y) => {+ });+ + draw_action.connect ((self, cairo_context, glyph) => {+ });+ + }+ + public static void set_grid_width (double w) {+ size_x = w;+ size_y = w;+ + updata_lines ();+ }+ + private static void updata_lines () {+ Glyph g = MainWindow.get_current_glyph ();+ double step = size_y * Glyph.SCALE;+ double i;+ int max_lines = 400;+ int n;+ Line t, l, u;++ Line baseline = g.get_line ("baseline");+ Line bottom_margin = g.get_line ("bottom margin");+ Line top_margin = g.get_line ("top margin");+ + Line left = g.get_line ("left");+ Line right = g.get_line ("right");++ while (vertical.length () > 0) vertical.delete_link (vertical.first ());+ while (horizontal.length () > 0) horizontal.delete_link (horizontal.first ());+ + n = 0;+ for (i = left.get_pos () - 7 * step; i <= right.get_pos () + 7 * step; i += step) { + l = new Line ("grid", i, Line.VERTICAL);+ l.set_moveable (false);+ l.set_color (0.2, 0.6, 0.2, 0.2);+ horizontal.append (l);+ + if (++n >= max_lines) {+ break;+ }+ }+ + n = 0;+ for (i = baseline.get_pos () - step; i >= top_margin.get_pos () - 7 * step; i -= step) {+ t = new Line ("grid", i, Line.HOROIZONTAL);+ t.set_moveable (false);+ t.set_color (0.2, 0.6, 0.2, 0.2);+ vertical.append (t); ++ if (++n >= max_lines) {+ break;+ }++ }+ + for (i = baseline.get_pos (); i <= bottom_margin.get_pos () + 7 * step; i += step) {+ u = new Line ("grid", i, Line.HOROIZONTAL);+ u.set_moveable (false);+ u.set_color (0.2, 0.6, 0.2, 0.2);+ vertical.append (u); ++ if (++n >= max_lines) {+ break;+ }++ } + }+ + /** Sets x and y the point closest on grid. */+ public static void tie (ref double x, ref double y) {+ tie_point (ref x, ref y, false);+ }++ public static void tie_coordinate (ref double x, ref double y) {+ tie_point (ref x, ref y, true);+ }+ + private static void tie_point (ref double x, ref double y, bool coordinate)+ requires (horizontal.length () > 0 && vertical.length () > 0)+ {+ double d, m;+ + Glyph g = MainWindow.get_current_glyph ();+ + Line xmin = horizontal.first ().data;+ Line ymin = vertical.first ().data;++ Line xpos;+ Line ypos;+ + if (!coordinate) {+ xpos = new Line ("", 0, Line.VERTICAL);+ ypos = new Line ("", 0, Line.HOROIZONTAL);++ xpos.set_move (true);+ ypos.set_move (true);+ + xpos.move_line_to (x, y, g.allocation);+ ypos.move_line_to (x, y, g.allocation);+ } else {+ xpos = new Line ("", x, Line.VERTICAL);+ ypos = new Line ("", y, Line.HOROIZONTAL);+ }++ m = double.MAX;++ foreach (Line line in vertical) {+ d = Math.fabs (line.get_pos () - ypos.get_pos ());+ + if (d <= m) {+ m = d;+ ymin = line;+ }+ + }++ m = double.MAX;+ foreach (Line line in horizontal) {+ d = Math.fabs (line.get_pos () - xpos.get_pos ());+ + if (d <= m) {+ m = d;+ xmin = line;+ }+ + }+ + if (!coordinate) {+ x = (xmin.get_pos () - g.view_offset_x + (g.allocation.width / 2)) * g.view_zoom; + y = (ymin.get_pos () - g.view_offset_y + (g.allocation.height / 2)) * g.view_zoom;+ } else {+ x = xmin.get_pos ();+ y = ymin.get_pos ();+ }+ }++ public static void tie_to_prev (EditPoint ep, double tx, double ty) {+ EditPoint pp;+ double a, b;+ + return_if_fail (ep.prev != null);+ + pp = ep.get_prev ().data;+ + ep.to_coordinate (ref tx, ref ty);+ + a = fabs (tx - pp.x);+ b = fabs (ty - pp.y);+ + if (a > b) {+ ep.y = pp.y;+ } else {+ ep.x = pp.x;+ }+ } ++ public static bool is_visible () {+ return visible;+ }+ + public static unowned List<Line> get_horizontal_lines () {+ return horizontal;+ }+ + public static unowned List<Line> get_vertical_lines () {+ return vertical;+ }++}++}+new file mode 100644@@ -0,0 +1,32 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class IOThread {++ public static void add (ThreadFunc<weak void*> func) {+ try {+ unowned Thread<void*> thread = Thread.create<void*> (func, false);+ } catch (Error e) {+ warning (@"e.message");+ }+ }+ +}++}new file mode 100644@@ -0,0 +1,88 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++using Cairo; ++class Icons {+ + static string icon_dir = "";+ + public static ImageSurface? get_icon (string? name) {+ ImageSurface? img = null;+ File f;+ + if (name == null) {+ warning ("Can't find a file for name \"null\".");+ return null;+ }++ f = find_icon ((!) name);+ + if (!f.query_exists ()) {+ warning (@"Can't load icon: $((!)f.get_path ())");+ return null;+ }+ + img = new ImageSurface.from_png ((!)f.get_path ());+ + return img;+ }+ + private static File find_icon (string name) {+ File f;++ f = find_file (icon_dir, name);+ if (likely (f.query_exists ())) return f;++ f = find_file ("/usr/local/share/icons/supplement/", name);+ if (likely (f.query_exists ())) return f;++ f = find_file ("/usr/share/icons/supplement/", name);+ if (likely (f.query_exists ())) return f;++ f = find_file ("./icons/", name);+ if (likely (f.query_exists ())) return f; ++ f = find_file ("../icons/", name);+ if (likely (f.query_exists ())) return f;++ f = find_file (".\\icons\\", name);+ if (likely (f.query_exists ())) return f;++ f = find_file ("", name);+ if (likely (f.query_exists ())) return f;++ f = find_file ("icons\\", name);+ if (likely (f.query_exists ())) return f;+ + return f;+ }+ + private static File find_file (string? path, string name) {+ StringBuilder fn = new StringBuilder ();+ string p = (path == null) ? "" : (!) path;+ + icon_dir = p;+ fn.append (icon_dir);+ fn.append ((!) name);+ return File.new_for_path (fn.str);+ }+}+ +}new file mode 100644@@ -0,0 +1,40 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Xml;++namespace Supplement {+class Kerning {+ public double val;+ public string left;+ public string right;+ + public Kerning (string l, string r, double v) {+ val = v;+ left = l;+ right = r;+ }+ + public Kerning.from_attribute (string l, string r, string xml_attr) {+ val = double.parse (xml_attr); + left = l;+ right = r;+ }+}++}new file mode 100644@@ -0,0 +1,541 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gdk;++namespace Supplement {++enum Key {+ NONE = 0,+ UP = 65362,+ RIGHT = 65363,+ DOWN = 65364,+ LEFT = 65361,+ PG_UP = 65365,+ PG_DOWN = 65366,+ ENTER = 65293,+ BACK_SPACE = 65288,+ SHIFT_LEFT = 65505,+ SHIFT_RIGHT = 65506,+ CTRL_LEFT = 65507,+ CTRL_RIGHT = 65508,+ CAPS_LOCK = 65509,+ ALT_LEFT = 65513,+ ALT_RIGHT = 65514,+ ALT_GR = 65027,+ LOGO_LEFT = 65515,+ LOGO_RIGHT = 65516,+ CONTEXT_MENU = 65383,+ TAB = 65289,+ DEL = 65535+}++bool is_modifier_key (uint i) {+ return Key.UP == i ||+ Key.RIGHT == i ||+ Key.DOWN == i ||+ Key.LEFT == i ||+ Key.PG_UP == i ||+ Key.PG_DOWN == i ||+ Key.ENTER == i ||+ Key.BACK_SPACE == i ||+ Key.SHIFT_LEFT == i ||+ Key.SHIFT_RIGHT == i ||+ Key.CTRL_LEFT == i ||+ Key.CTRL_RIGHT == i ||+ Key.ALT_LEFT == i ||+ Key.ALT_RIGHT == i ||+ Key.ALT_GR == i || + Key.LOGO_LEFT == i || + Key.LOGO_RIGHT == i || + Key.TAB == i || + Key.CAPS_LOCK == i || + Key.LOGO_RIGHT == i;+}++/** Modifier flags */+public static const uint NONE = 0;+public static const uint CTRL = 1 << 0;+public static const uint ALT = 1 << 2;+public static const uint SHIFT = 1 << 3;++public bool is_mod (EventKey e) {+ return (e.keyval == Key.CTRL_RIGHT || e.keyval == Key.CTRL_LEFT || e.keyval == Key.ALT_RIGHT || e.keyval == Key.ALT_LEFT);+}++/** A list of all valid key bindings. */+class BindingList {+ static ShortCut[]? nc = null;+ + public static ShortCut[] get_default_bindings () { + + if (nc == null) {+ + // add all bindings to this list+ ShortCut[] default_bindings = { + new ShowMenu (),+ new ToggleToolBar (),+ new NextTab (),+ new PreviousTab (),+ new CloseTab (),+ new UndoAction (),+ new Copy (),+ new Paste ()+ };++ nc = default_bindings;+ }+ return nc;+ }+}++class Paste : ShortCut {+ + public override void run () {+ ClipTool.paste ();+ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return 'v'; } + public override unowned string get_description () { return "Paste"; }+}++class Copy : ShortCut {+ + public override void run () {+ ClipTool.copy ();+ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return 'c'; } + public override unowned string get_description () { return "Copy"; }+}++class UndoAction : ShortCut {+ + public override void run () {+ MainWindow.get_current_display ().undo ();+ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return 'z'; } + public override unowned string get_description () { return "Undo"; }+}++class ShowMenu : ShortCut {+ + public override void run () {+ MainWindow.get_tab_bar ().select_tab_name ("Content");+ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return 'f'; } + public override unowned string get_description () { return "Show menu"; }+}++class ToggleToolBar : ShortCut {+ + public override void run () {+ Toolbox t = MainWindow.get_toolbox ();+ + if (t.is_expanded ()) {+ t.minimize ();+ } else {+ t.maximize ();+ }++ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return 'b'; } + public override unowned string get_description () { return "Show toolbox"; }+}++class NextTab : ShortCut {+ + public override void run () {+ TabBar tb = MainWindow.get_tab_bar ();+ int n = tb.get_selected () + 1;+ + if (!(0 <= n < tb.get_length ())) {+ return;+ }+ + tb.select_tab (n);+ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return Key.RIGHT; } + public override unowned string get_description () { return "Next tab"; }+}++class PreviousTab : ShortCut {+ + public override void run () {+ TabBar tb = MainWindow.get_tab_bar ();+ int n = tb.get_selected () - 1;++ if (!(0 <= n < tb.get_length ())) {+ return;+ }+ + tb.select_tab (n);+ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return Key.LEFT; } + public override unowned string get_description () { return "Previous tab"; }+}++class CloseTab : ShortCut {+ + public override void run () {+ TabBar tb = MainWindow.get_tab_bar ();+ int n = tb.get_selected ();++ if (!(0 <= n < tb.get_length ())) {+ return;+ }+ + tb.close_tab (n);+ }+ + public override uint get_default_modifier () { return CTRL; }+ public override uint get_default_key () { return (uint) 'w'; } + public override unowned string get_description () { return "Close tab"; }+}++/** Function to be executed for each global key binding. */+abstract class ShortCut : GLib.Object {+ uint modifier = 0;+ uint key = 0;+ + public abstract void run ();+ + public void set_modifier (uint m) {+ modifier = m;+ }++ public void set_key (uint k) {+ key = k;+ }+ + public uint get_modifier () {+ return modifier;+ }+ + public uint get_key () {+ return key;+ }+ + public abstract uint get_default_modifier ();+ public abstract uint get_default_key ();+ + public abstract unowned string get_description ();+ + public static EqualFunc<ShortCut> equal = (a, b) => {+ if (a.get_key () != b.get_key ()) return false;+ if (a.get_modifier () != b.get_modifier ()) return false;+ if (a.get_description () != b.get_description ()) return false;+ return true;+ };+ +}++class KeyBindings {+ + bool modifier_ctrl = false;+ bool modifier_alt = false;+ bool modifier_shift = false;+ + public static uint modifier = 0;+ + bool require_modifier = false;+ + /** First uint is modifer flag, second uint is keyval */+ HashTable<uint, HashTable<uint, ShortCut>> short_cuts = new HashTable<uint, HashTable<uint, ShortCut>> (null, null);+ + public static KeyBindings singleton;+ + public KeyBindings () {+ + HashFunc<uint> hash = (v) => {+ return v;+ };+ + HashTable<uint, ShortCut> alt_ctrl_shift = new HashTable<uint, ShortCut> (hash, ShortCut.equal);+ HashTable<uint, ShortCut> alt_shift = new HashTable<uint, ShortCut> (hash, ShortCut.equal);+ HashTable<uint, ShortCut> ctrl_shift = new HashTable<uint, ShortCut> (hash, ShortCut.equal);+ HashTable<uint, ShortCut> alt = new HashTable<uint, ShortCut> (hash, ShortCut.equal);+ HashTable<uint, ShortCut> ctrl = new HashTable<uint, ShortCut> (hash, ShortCut.equal);+ + // Possible modifiers+ short_cuts.insert (ALT|CTRL|SHIFT, alt_ctrl_shift);+ short_cuts.insert (ALT|SHIFT, alt_shift);+ short_cuts.insert (CTRL|SHIFT, ctrl_shift);+ short_cuts.insert (ALT, alt);+ short_cuts.insert (CTRL, ctrl);+ + // Add default bindings+ foreach (var b in BindingList.get_default_bindings ()) {+ add_binding (b);+ }+ + save ();+ load ();+ + singleton = this;+ }+ + public void reset () {+ modifier = NONE;+ modifier_ctrl = false;+ modifier_alt = false;+ modifier_shift = false;+ }+ + public void set_require_modifier (bool m) {+ require_modifier = m;+ }+ + void load () {+ File home = File.new_for_path (Environment.get_home_dir ());+ File settings = home.get_child (".supplement");+ File bindings = settings.get_child ("keybindings");++ if (!bindings.query_exists ()) {+ return;+ }++ FileStream? bindings_file = FileStream.open ((!) bindings.get_path (), "r");+ + if (bindings_file == null) {+ stderr.printf ("Failed to load keybindings from file %s.\n", (!) bindings.get_path ());+ return;+ }+ + return_if_fail (bindings_file != null);+ + unowned FileStream b = (!) bindings_file;+ + string? l;+ l = b.read_line ();+ while ((l = b.read_line ())!= null) {+ string line;+ + line = (!) l;+ + if (line.get_char (0) == '#') {+ continue;+ }+ + int i = 0;+ int s = 0;+ + i = line.index_of_char(' ', s);+ string mod = line.substring (s, i - s);+ + s = i + 1;+ i = line.index_of_char(' ', s);+ string key = line.substring (s, i - s);+ + s = i + 1;+ i = line.index_of_char('\n', s);+ string description = line.substring (s, i - s);++ update_short_cut (mod, key, description);+ }+ + }+ + void save () {+ try {+ File settings = Supplement.get_settings_directory ();+ File bindings = settings.get_child ("keybindings");+ + if (bindings.query_exists ()) {+ bindings.delete ();+ }++ DataOutputStream os = new DataOutputStream(bindings.create(FileCreateFlags.REPLACE_DESTINATION));+ uint8[] data;+ long written = 0;+ + StringBuilder sb = new StringBuilder ();+ + sb.append_printf ("# Supplement keybindings\n");+ sb.append_printf ("# Version: 1.0\n");+ + short_cuts.foreach ( (k, v) => {+ v.foreach ( (k, s) => {+ sb.append_printf ("%u %u %s\n", s.get_modifier (), s.get_key (), s.get_description ());+ });+ });+ + data = sb.str.data;+ + while (written < data.length) { + written += os.write (data[written:data.length]);+ }+ + } catch (Error e) {+ stderr.printf ("Can not save key bindings. (%s)", e.message); + }+ + }+ + void update_short_cut (string mod, string key, string description) {+ ShortCut? sc = null;+ ShortCut binding;+ short_cuts.foreach ( (k, ht) => {+ ht.foreach ((ki, s) => {+ if (s.get_description () == description) {+ sc = s;+ }+ + // ht.remove (ki); // FIXME! why does it remove all items?+ + });+ });+ + if (unlikely (sc == null)) {+ stderr.printf ("Can not set bindings for \"%s-%s-%s-\"\n", mod, key, description);+ return;+ }+ + binding = (!) sc;++ binding.set_modifier ((uint) uint64.parse (mod));+ binding.set_key ((uint) uint64.parse (key));+ + add_binding (binding);+ }+ + void add_binding (ShortCut binding) {+ uint m = binding.get_modifier ();+ + if (m == 0) {+ m = binding.get_default_modifier ();+ binding.set_modifier (m);+ }++ if (binding.get_key () == 0) {+ binding.set_key (binding.get_default_key ());+ }+ + HashTable<uint, ShortCut>? e = short_cuts.lookup (m);+ + if (e == null) {+ stderr.printf ("Invalid modifier in key binding: expecting ctrl, alt or shift. Flag: (%u) \n", modifier);+ return;+ }+ + var events = (!) e;+ events.insert (binding.get_key (), binding);+ }+ + bool get_short_cut (uint modifier, uint key, out ShortCut? sc) {+ sc = null;+ + foreach (var v in BindingList.get_default_bindings ()) {+ if (v.get_default_modifier () == modifier && v.get_default_key () == key) {+ sc = v;+ return true;+ }+ }+ + return false;+ }+ + void set_modifier (uint k, bool v) {+ switch (k) {+ case Key.CTRL_LEFT:+ modifier_ctrl = v;+ break;+ case Key.CTRL_RIGHT:+ modifier_ctrl = v;+ break;+ case Key.ALT_LEFT:+ modifier_alt = v;+ break;+ case Key.ALT_GR:+ modifier_alt = v;+ break;+ case Key.SHIFT_LEFT:+ modifier_shift = v;+ break;+ case Key.SHIFT_RIGHT:+ modifier_shift = v;+ break;+ }+ + modifier = 0;++ if (modifier_ctrl) modifier |= CTRL;+ if (modifier_alt) modifier |= ALT; + if (modifier_shift) modifier |= SHIFT;+ }++ public static bool has_alt () {+ return singleton.modifier_alt;+ }+ + public static bool has_shift () {+ return singleton.modifier_shift;+ }+ + public static bool has_ctrl () {+ return singleton.modifier_ctrl;+ }+ + public void key_press (EventKey event) {+ bool press = (event.type == EventType.KEY_PRESS);+ uint key = event.keyval;+ ShortCut? short_cut;+ + set_modifier (key, press);+ + if (!press) {+ return;+ }+ + if (get_short_cut (modifier, key, out short_cut)) {+ if (short_cut != null)+ ((!)short_cut).run ();+ }+ + foreach (var exp in MainWindow.get_toolbox ().expanders) {+ foreach (Tool t in exp.tool) {+ t.set_active (false);+ + if (t.key == event.keyval && t.modifier_flag == modifier) {+ if (!(require_modifier && (modifier == NONE || modifier == SHIFT))) {+ MainWindow.get_toolbox ().select_tool (t);+ }+ }+ }+ }+ + foreach (Tool t in MainWindow.get_content_display ().tools) { + if (t.key == event.keyval && t.modifier_flag == modifier) {+ t.select_action (t);+ }+ }+ }+}++}new file mode 100644@@ -0,0 +1,354 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;++namespace Supplement {++/** Help line */+class Line : GLib.Object {+ public static const bool VERTICAL = true;+ public static const bool HOROIZONTAL = false;+ + string label;+ bool vertical;+ + public double pos;+ + bool active = false;+ bool move = false;+ + public signal void queue_draw_area (int x, int y, int w, int h);+ public signal void position_updated (double pos);+ + public unowned List<Line>? list_item = null;+ + double r;+ double g;+ double b;+ double a;+ + bool visible = true;+ bool moveable = true;+ + public Line (string label, double position = 10, bool vertical = false) {+ this.label = label;+ this.vertical = vertical;+ this.pos = position;+ + r = 0.7;+ g = 0.7;+ b = 0.8;+ a = 1;+ }+ + public Line copy () {+ Line l = new Line (label, pos, vertical);+ + l.r = r;+ l.g = g;+ l.b = b;+ l.a = a;+ + l.visible = visible;+ + return l;+ }+ + public void set_visible (bool v) {+ visible = v;+ }+ + public bool get_visible (bool v) {+ return visible;+ }+ + public void set_pos (double p) {+ pos = p;+ }+ + public void set_moveable (bool m) {+ moveable = m;+ }+ + public void set_color (double r, double g, double b, double a) {+ this.r = r;+ this.g = g;+ this.b = b;+ this.a = a;+ }+ + // Valabug: should be public “void set_list_item (unowned List<Line> ll) {”+ // Keeping list_item public for now+ public void set_list_item (void* ll) {+ // list_item = ll;+ }+ + public bool is_moving () {+ return move;+ }+ + public bool set_move (bool moving) {+ bool r = move;+ move = moving;+ return (r == moving);+ }+ + public bool button_press (EventButton e, Allocation allocation) {+ Glyph g;+ if (get_active ()) {+ move = true;+ + g = MainWindow.get_current_glyph ();+ g.store_undo_state ();+ + return true;+ }+ + return false;+ }++ public void redraw_line () {+ double p;+ Glyph g = MainWindow.get_current_glyph ();+ + if (vertical) {+ p = (pos - g.view_offset_x + (g.allocation.width / 2.0)) * g.view_zoom;+ queue_draw_area ((int)p - 10, 0, 200, g.allocation.height);+ } else {+ p = (pos - g.view_offset_y + (g.allocation.height / 2.0)) * g.view_zoom;+ queue_draw_area (0, (int)p - 10, g.allocation.width, 300);+ }+ } + + public void move_line_to (double x, double y, Allocation allocation) {+ set_move (true);+ event_move_to (x, y, x, y, allocation);+ }+ + /** Move to line to @param x, @param y and execute ove line handle + * event for @param x, @param y.+ */+ public void event_move_to (double x, double y, double ax, double ay, Allocation allocation) {+ double p, c, mx, my;+ bool a = false;+ Glyph g = MainWindow.get_current_glyph ();+ double ivz = 1/g.view_zoom;+ double margin = 10;++ if (!moveable) return;++ if (move) {+ double np = pos;+ + redraw_line (); // clear old position+ + if (is_vertical ()) {+ pos = x * ivz + g.view_offset_x - (g.allocation.width / 2.0);+ redraw_line (); // draw at new position+ } else {+ pos = y * ivz + g.view_offset_y - (g.allocation.height / 2.0);+ redraw_line ();+ }++ if (Math.fabs (np - pos) > 10) {+ queue_draw_area (0, 0, g.allocation.width, g.allocation.height);+ }++ position_updated (pos); // sinal update++ Supplement.get_current_font ().touch ();+ + swap_lines ();+ }++ if (is_vertical ()) {+ if (ay > g.allocation.height - 10) {+ + p = get_coordinate ();++ c = ax * ivz + g.view_offset_x;+ a = (p - margin * ivz <= c <= p + margin * ivz);+ }+ + if (a != get_active ()) {+ redraw_line ();+ }++ set_active (a);+ + } else {+ if (ax > g.allocation.width - 10) {+ p = get_coordinate ();+ c = ay * ivz + g.view_offset_y;+ a = (p - margin * ivz <= c <= p + margin * ivz);+ }+ + if (a != get_active ()) {+ redraw_line ();+ }+ + set_active (a);+ }+ }+ + private void swap_lines () {+ double ivz = 1 / MainWindow.get_current_glyph ().view_zoom;+ + if (!moveable) return;+ + // switch to correct line if this line passes the next one in the sorted list+ if (list_item != null) {+ unowned List<Line> ll = (!) list_item;+ + if (ll != ll.first ()) {+ if (ll.prev.data.pos > pos) {+ ll.prev.data.set_move (true);+ ll.prev.data.set_visible (true);+ set_move (false);+ pos = ll.prev.data.pos;+ ll.prev.data.pos += 1 * ivz;+ }+ }+ + if (ll != ll.last ()) {+ if (ll.next.data.pos < pos) {+ ll.next.data.set_move (true);+ ll.next.data.set_visible (true);+ pos = ll.next.data.pos;+ ll.next.data.pos -= 1 * ivz;+ set_move (false);+ }+ }+ + }+ + }+ + public bool get_active () {+ return active;+ }+ + protected void set_active (bool active) {+ this.active = active;+ }+ + public string get_label () {+ return label;+ }+ + public bool is_vertical () {+ return vertical;+ }++ public double get_coordinate () {+ Glyph g = MainWindow.get_current_glyph ();+ double t = (is_vertical ()) ? (g.allocation.width / 2.0) : (g.allocation.height / 2.0);+ return pos + t;+ }+ + public double get_pos () {+ return pos;+ }+ + private double get_handle_size () {+ Glyph g = MainWindow.get_current_glyph ();+ double ivz = 1/g.view_zoom;+ return (get_active ()) ? (10 * ivz) : (5 * ivz);+ }+ + public void draw (Context cr, Allocation allocation) {+ Glyph g = MainWindow.get_current_glyph ();+ + double p, h, w;+ double ivz = 1/g.view_zoom;++ double size = get_handle_size ();+ + if (!visible) return;+ + cr.save ();+ cr.set_line_width (ivz);+ + if (active) cr.set_source_rgba (0, 0, 0.3, 1);+ else cr.set_source_rgba (r, this.g, b, a);+ + // Line+ if (is_vertical ()) {+ p = get_coordinate ();+ + h = g.allocation.height * ivz + g.view_offset_y;+ + cr.move_to (p, g.view_offset_y);+ cr.line_to (p, h);+ cr.stroke ();++ cr.scale (1, 1);++ if (moveable) {+ cr.new_path ();+ cr.move_to (p - size, h); + cr.line_to (p, h - size); + cr.line_to (p + size, h);+ cr.close_path();+ cr.fill ();+ }+ + } else {+ p = get_coordinate ();+ + w = g.allocation.width * ivz + g.view_offset_x;+ + cr.move_to (g.view_offset_x, p);+ cr.line_to (w, p); + cr.stroke ();+ + if (moveable) {+ cr.new_path ();+ cr.move_to (w, p - size); + cr.line_to (w - size, p); + cr.line_to (w, p + size);+ cr.close_path();+ cr.fill ();+ }+ }+ + // Label+ if (get_active ()) { + if (is_vertical ()) {+ h = g.allocation.height * ivz + g.view_offset_y;+ cr.move_to (p + 8*ivz, h - 30*ivz);+ } else {+ w = g.allocation.width * ivz + g.view_offset_x;+ cr.move_to (w - 70*ivz, p + 15*ivz);+ }++ cr.set_font_size (12 * ivz);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + cr.show_text (get_label ());+ cr.stroke ();+ }+ + cr.restore ();+ }++}++}new file mode 100644@@ -0,0 +1,270 @@+/* + Copyright (C) 2012 Johan Mattsson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +using Gdk; +using Gtk; + +namespace Supplement { + +public class MainWindow : Gtk.Window { + + HBox hb; + + static TabBar tabs; + VBox tab_box; + + static Toolbox tools; + static GlyphCanvas glyph_canvas; + static MainWindow singleton; + static KeyBindings key_bindings; + static ContentDisplay content; + static TooltipArea tool_tip; + + static DrawingArea margin_bottom; + static DrawingArea margin_right; + + public MainWindow(string title) { + singleton = this; + + key_bindings = new KeyBindings (); + + glyph_canvas = new GlyphCanvas (); + tools = new Toolbox (glyph_canvas); + tabs = new TabBar (); + content = new ContentDisplay (); + tool_tip = new TooltipArea (); + + margin_bottom = new DrawingArea (); + margin_right = new DrawingArea (); + + margin_bottom.set_size_request (0, 0); + margin_right.set_size_request (0, 0); + + set_title (title); + + delete_event.connect (quit); + + set_size_and_position (); + + tabs.signal_tab_selected.connect ((f, tab) => { + FontDisplay fd = tab.get_display (); + glyph_canvas.set_current_glyph (fd); + }); + + OverView over_view = new OverView(); + + tabs.add_unique_tab (content, 60, true); + tabs.add_unique_tab (over_view, 75, false); + + tabs.select_tab_name ("Content"); + + hb = new HBox (false, 0); + + tab_box = new VBox (false, 0); + tab_box.pack_start (tabs, false, false, 0); + tab_box.pack_start (glyph_canvas, true, true, 0); + tab_box.pack_start (tool_tip, false, false, 0); + tab_box.pack_start (margin_bottom, false, false, 0); + + hb.pack_start (tab_box, true, true, 0); + hb.pack_start (tools, false, false, 0); + hb.pack_start (margin_right, false, false, 0); + + add (hb); + + key_snooper_install (global_key_bindings, null); + + add_events (EventMask.FOCUS_CHANGE_MASK); + + focus_in_event.connect ((t, e)=> { + key_bindings.reset (); + return true; + }); + + show_all (); + } + + public static void hide_cursor () { + Pixmap pixmap = new Pixmap (null, 1, 1, 1); + Color color = { 0, 0, 0, 0 }; + Cursor cursor = new Cursor.from_pixmap (pixmap, pixmap, color, color, 0, 0); + + // Fixa: But why? + // (Supplement.exe:1300): Gdk-CRITICAL **: gdk_window_set_cursor: assertion `GDK_IS_WINDOW (window)' failed + // singleton.frame.set_cursor (cursor); + } + + private void set_size_and_position () { + int w = Preferences.get_window_width(); + int h = Preferences.get_window_height(); + + set_default_size (w, h); + // move (10, 240); + } + + public bool quit () { + bool added; + SaveDialog s = new SaveDialog (); + + if (Supplement.get_current_font ().is_modified ()) { + added = tabs.add_unique_tab (s, 50); + } else { + added = false; + } + + if (!added) { + Supplement.get_current_font ().save_backup (); + Gtk.main_quit (); + } + + s.finished.connect (() => { + Supplement.get_current_font ().delete_backup (); + Gtk.main_quit (); + }); + + return true; + } + + internal static MainWindow get_current_window () { + return singleton; + } + + internal static FontDisplay get_current_display () { + return get_glyph_canvas ().get_current_display (); + } + + internal static GlyphCanvas get_glyph_canvas () { + return glyph_canvas; + } + + internal static Glyph get_current_glyph () { + Glyph? g = get_singleton ().glyph_canvas.get_current_glyph (); + + if (unlikely (g == null)) { + warning ("No default glyph have been set yet.\n"); + return new Glyph ("no_glyph_created"); + } + + return (!) g; + } + + internal static Toolbox get_toolbox () { + return get_singleton ().tools; + } + + internal static Tool get_tool (string n) { + return get_singleton ().tools.get_tool (n); + } + + internal static TabBar get_tab_bar () { + return get_singleton ().tabs; + } + + internal static Tab get_current_tab () { + return get_singleton ().tabs.get_selected_tab (); + } + + internal static TooltipArea get_tool_tip () { + return tool_tip; + } + + internal static bool select_tab (Tab t) { + return get_singleton ().tabs.selected_open_tab (t); + } + + internal static OverView get_overview () { + OverView over_view; + + foreach (var t in tabs.tabs) { + if (t.get_display () is OverView) { + return (OverView) t.get_display (); + } + } + + over_view = new OverView(); + tabs.add_unique_tab (over_view, 75, false); + + return over_view; + } + + internal static ContentDisplay get_content_display () { + if (unlikely ((ContentDisplay?) get_singleton ().content == null)) { + warning ("ContentDisplay not instantiated."); + } + + return get_singleton ().content; + } + + public static MainWindow get_singleton () { + return singleton; + } + + public static int global_key_bindings (Widget grab_widget, EventKey event, void* data) { + MainWindow window = get_singleton (); + + window.glyph_canvas.key_press (event); + window.key_bindings.key_press (event); + + return 0; + } + + /** Reaload all paths and help lines from disk. */ + internal static void clear_glyph_cache () { + Glyph g; + foreach (Tab t in get_tab_bar ().tabs) { + if (t.get_display () is Glyph) { + g = (Glyph) t.get_display (); + g.add_help_lines (); + } + } + + get_glyph_canvas ().redraw (); + } + + internal static void close_all_tabs () { + uint i = 0; + uint len = get_tab_bar ().get_length (); + + while (i < len) { + if (!get_tab_bar ().close_tab ((int) i)) { + i++; + } + } + } + + internal static void toggle_expanded_margin_bottom () { + int w, h; + margin_bottom.get_size_request (out w, out h); + + if (h == 1) h = 2; + else h = 1; + + margin_bottom.set_size_request (w, h); + } + + internal static void toggle_expanded_margin_right () { + int w, h; + margin_right.get_size_request (out w, out h); + + if (w == 1) w = 2; + else w = 1; + + margin_right.set_size_request (w, h); + } +} + +} new file mode 100644@@ -0,0 +1,533 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class MergeTool : Tool {++ public MergeTool (string n) {+ base (n, "merge paths");+ + select_action.connect((self) => {+ print ("merge_all");+ merge_all ();+ });+ }++ private void merge_all () {+ Glyph g = MainWindow.get_current_glyph ();+ g.merge ();+ }++ public override bool test () {+ test_merge_second_triangle ();+ test_merge_simple_path_box_and_triangle ();+ test_merge_simple_path_box ();+ test_merge_odd_paths ();+ return true;+ }+ + private void test_merge_simple_path_box () {+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");++ this.yield ();+ MainWindow.get_tab_bar ().select_overview ();++ this.yield ();+ MainWindow.get_overview ().open_current_glyph ();+ + pen_tool.test_select_action ();++ int x_offset = 10;+ int y_offset = 350;+ + // draw it+ pen_tool.test_click_action (1, 10 + x_offset, 10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset); + pen_tool.test_click_action (3, 0, 0);++ pen_tool.test_press_action (1, 1, 1);+ pen_tool.test_move_action (15 + x_offset, 15 + y_offset);+ pen_tool.test_release_action (1, 15 + x_offset, 15 + y_offset);+ + pen_tool.test_click_action (1, 25 + x_offset, 15 + y_offset);+ pen_tool.test_click_action (1, 25 + x_offset, 25 + y_offset);+ pen_tool.test_click_action (1, 15 + x_offset, 25 + y_offset); + pen_tool.test_click_action (3, 0, 0);+ + // merge it+ this.test_select_action ();+ + // test result+ Path merged_outline = new Path ();+ add_point_on_path (merged_outline, 10 + x_offset, 10 + y_offset);+ add_point_on_path (merged_outline, 20 + x_offset, 10 + y_offset);+ add_point_on_path (merged_outline, 20 + x_offset, 15 + y_offset);+ add_point_on_path (merged_outline, 25 + x_offset, 15 + y_offset);+ add_point_on_path (merged_outline, 25 + x_offset, 25 + y_offset);+ add_point_on_path (merged_outline, 15 + x_offset, 25 + y_offset);+ add_point_on_path (merged_outline, 15 + x_offset, 20 + y_offset);+ add_point_on_path (merged_outline, 10 + x_offset, 20 + y_offset);+ merged_outline.close ();+ + // select path+ pen_tool.test_click_action (3, 12 + x_offset, 12 + y_offset);+ + Glyph g = MainWindow.get_current_glyph ();+ Path? l = g.get_active_path ();+ + if (l == null) {+ critical ("No path found in merge test, it did not merge correctly.");+ return;+ }+ + Path last = (!) l;+ bool merged_path_looks_good = false;+ merged_path_looks_good = last.test_is_outline (merged_outline);+ + if (!merged_path_looks_good) critical ("Failed to merge path correctly.");+ }++ private void test_merge_simple_path_box_and_triangle () {+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");++ this.yield ();+ MainWindow.get_tab_bar ().select_overview ();++ this.yield ();+ MainWindow.get_overview ().open_current_glyph ();+ + pen_tool.test_select_action ();++ int x_offset = 40;+ int y_offset = 350;+ + // draw it+ pen_tool.test_click_action (1, 0 + x_offset, -50 + y_offset);+ pen_tool.test_click_action (1, 25 + x_offset, -50 + y_offset);+ pen_tool.test_click_action (1, 25 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (1, 0 + x_offset, 0 + y_offset); + pen_tool.test_click_action (3, 0, 0);+ + pen_tool.test_click_action (1, 0 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (1, 50 + x_offset, -50 + y_offset);+ pen_tool.test_click_action (1, 50 + x_offset, 0 + y_offset); + pen_tool.test_click_action (3, 0, 0);++ // merge it+ this.test_select_action ();+ + // test result+ Path merged_outline = new Path ();+ add_point_on_path (merged_outline, 0 + x_offset, -50 + y_offset);+ add_point_on_path (merged_outline, 25 + x_offset, -50 + y_offset);+ add_point_on_path (merged_outline, -50 + x_offset, -50 + y_offset);+ add_point_on_path (merged_outline, -50 + x_offset, 0 + y_offset);+ add_point_on_path (merged_outline, 0 + x_offset, 0 + y_offset);+ merged_outline.close ();++ // select path+ pen_tool.test_click_action (3, 21 + x_offset, -11 + y_offset);+ + Glyph g = MainWindow.get_current_glyph ();+ Path? l = g.get_active_path ();+ + if (l == null) {+ critical ("No path found in test_merge_simple_path_box_and_triangle, it did not merge correctly.");+ return;+ }+ + Path last = (!) l;+ bool merged_path_looks_good = false;+ merged_path_looks_good = last.test_is_outline (merged_outline);+ + if (!merged_path_looks_good) critical ("Failed to merge path correctly.");+ }++ private void test_merge_second_triangle () {+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");++ this.yield ();+ MainWindow.get_tab_bar ().select_overview ();++ this.yield ();+ MainWindow.get_overview ().open_current_glyph ();+ + pen_tool.test_select_action ();++ int x_offset = 100;+ int y_offset = 350;+ + // draw it+ pen_tool.test_click_action (1, 25 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (1, 25 + x_offset, -50 + y_offset);+ pen_tool.test_click_action (1, 0 + x_offset, -50 + y_offset);+ pen_tool.test_click_action (1, 0 + x_offset, 0 + y_offset); ++ pen_tool.test_click_action (3, 0, 0);+ + pen_tool.test_click_action (1, 40 + x_offset, -50 + y_offset);+ pen_tool.test_click_action (1, 10 + x_offset, -20 + y_offset);+ pen_tool.test_click_action (1, 25 + x_offset, -20 + y_offset); + pen_tool.test_click_action (3, 0, 0);+ + // merge it+ this.test_select_action ();+ + // test result+ Path merged_outline = new Path ();+ add_point_on_path (merged_outline, 0 + x_offset, -50 + y_offset);+ add_point_on_path (merged_outline, 25 + x_offset, -50 + y_offset);+ add_point_on_path (merged_outline, -50 + x_offset, -50 + y_offset);+ add_point_on_path (merged_outline, -50 + x_offset, 0 + y_offset);+ add_point_on_path (merged_outline, 0 + x_offset, 0 + y_offset);+ merged_outline.close ();++ // select path+ pen_tool.test_click_action (3, 21 + x_offset, -11 + y_offset);+ + Glyph g = MainWindow.get_current_glyph ();+ Path? l = g.get_active_path ();+ + if (l == null) {+ critical ("No path found in triangle_right, it did not merge correctly.");+ return;+ }+ + Path last = (!) l;+ bool merged_path_looks_good = false;+ merged_path_looks_good = last.test_is_outline (merged_outline);+ + if (!merged_path_looks_good) critical ("Failed to merge path correctly.");+ }+ + private void add_point_on_path (Path p, int x, int y) {+ Glyph g = MainWindow.get_current_glyph ();+ p.add (g.path_coordinate_x (x), g.path_coordinate_y (y));+ }+ + + private void test_merge_odd_paths () {+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");++ this.yield ();+ MainWindow.get_tab_bar ().select_overview ();++ this.yield ();+ MainWindow.get_overview ().open_current_glyph ();+ + pen_tool.test_select_action ();++ // paint+ int x_offset = 100;+ int y_offset = 100;+ + // rectangle+ pen_tool.test_click_action (1, 100 + x_offset, 100 + y_offset);+ pen_tool.test_click_action (1, 170 + x_offset, 100 + y_offset);+ pen_tool.test_click_action (1, 170 + x_offset, 120 + y_offset);+ pen_tool.test_click_action (1, 100 + x_offset, 120 + y_offset); + pen_tool.test_click_action (3, 0, 0); // close+ + // triangle+ pen_tool.test_click_action (1, 100 + x_offset, 110 + y_offset);+ pen_tool.test_click_action (1, 180 + x_offset, 130 + y_offset);+ pen_tool.test_click_action (1, -10 + x_offset, 140 + y_offset);+ pen_tool.test_click_action (3, 0, 0);++ // several triangles+ pen_tool.test_click_action (1, 198, 379); + pen_tool.test_click_action (1, 274, 328); + pen_tool.test_click_action (1, 203, 286); + pen_tool.test_click_action (3, 230, 333);++ pen_tool.test_click_action (1, 233, 429); + pen_tool.test_click_action (1, 293, 382); + pen_tool.test_click_action (1, 222, 322); + pen_tool.test_click_action (3, 225, 406);++ pen_tool.test_click_action (1, 164, 316); + pen_tool.test_click_action (1, 262, 289); + pen_tool.test_click_action (1, 203, 260); + pen_tool.test_click_action (3, 203, 260); ++ this.yield ();+ + test_select_action ();+ }+ + private void test_last_is_counter_clockwise (string name) {+ bool d = ((!)MainWindow.get_current_glyph ().get_last_path ()).is_clockwise ();+ + if (d) {+ critical (@"\nPath $name is clockwise, in test_last_is_counter_clockwise");+ } + }+ + private void test_last_is_clockwise (string name) {+ bool d = ((!)MainWindow.get_current_glyph ().get_last_path ()).is_clockwise ();+ + if (!d) {+ critical (@"\nPath $name is counter clockwise, in test_last_is_clockwise");+ }++ }+ + private bool test_reverse_last (string name) + requires (MainWindow.get_current_glyph ().get_last_path () != null)+ {+ Glyph g = MainWindow.get_current_glyph ();+ Path p = (!) g.get_last_path ();+ bool direction = p.is_clockwise ();++ p.reverse ();+ + if (direction == p.is_clockwise ()) {+ critical (@"Direction did not change after reverseing path \"$name\"\n");+ stderr.printf (@"Path length: $(p.points.length ()) \n");+ return false;+ }++ this.yield ();+ return true;+ }+ + + class Point {+ + public int x;+ public int y;+ + public Point (int x, int y) {+ this.x = x;+ this.y = y;+ }+ }+ + private Point p (int x, int y) {+ return new Point (x, y);+ }+ + private void test_triangle (Point a, Point b, Point c, string name = "") {+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");+ + this.yield ();+ MainWindow.get_tab_bar ().select_overview ();++ this.yield ();+ MainWindow.get_overview ().open_current_glyph ();+ + pen_tool.test_select_action ();+ + pen_tool.test_click_action (1, a.x, a.y);+ pen_tool.test_click_action (1, b.x, b.y);+ pen_tool.test_click_action (1, c.x, c.y);+ pen_tool.test_click_action (3, 0, 0);+ + test_reverse_last (@"Triangle reverse \"$name\" ($(a.x), $(a.y)), ($(b.x), $(b.y)), ($(c.x), $(c.y)) failed.");+ + this.yield ();+ }+ + private void test_various_triangles () {+ test_triangle (p (287, 261), p (155, 81), p (200, 104), "First");+ test_triangle (p (65, 100), p (168, 100), p (196, 177), "Second");+ test_triangle (p (132, 68), p (195, 283), p (195, 222), "Third");+ test_triangle (p (144, 267), p (147, 27), p (296, 267), "Fourth");+ test_triangle (p (65, 28), p (247, 24), p (187, 28), "Fith");+ }+ + public bool test_reverse_path () {+ // open a new glyph+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");++ this.yield ();+ MainWindow.get_tab_bar ().select_overview ();++ this.yield ();+ MainWindow.get_overview ().open_current_glyph ();+ + pen_tool.test_select_action ();+ + // paint+ int x_offset = 10;+ int y_offset = 10;+ + test_open_next_glyph ();+ test_various_triangles ();+ + test_open_next_glyph ();+ // draw clockwise and check direction ++ y_offset += 160;+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (1, 17 + x_offset, 17 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_last_is_clockwise ("Clockwise triangle 1.2");++ y_offset += 30;+ pen_tool.test_click_action (1, 0 + x_offset, 10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (1, 0 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_last_is_counter_clockwise ("Rectangle 1");+ + y_offset += 30;+ pen_tool.test_click_action (1, 0 + x_offset, 10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (1, 0 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_last_is_counter_clockwise ("Rectangle 2");++ pen_tool.test_click_action (1, 100, -1000 + y_offset);+ pen_tool.test_click_action (1, 115, -1000 + y_offset);+ pen_tool.test_click_action (1, 115, -2000 + y_offset);+ pen_tool.test_click_action (1, 120, -2000 + y_offset);+ pen_tool.test_click_action (1, 120, 1000 + y_offset);+ pen_tool.test_click_action (1, 100, 1000 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_last_is_clockwise ("Hight 3000");++ // draw paths clockwise / counter clockwise and reverse them+ + pen_tool.test_click_action (1, 115, 137);+ pen_tool.test_click_action (1, 89, 74);+ pen_tool.test_click_action (1, 188, 232);+ pen_tool.test_click_action (3, 0, 0);+ test_reverse_last ("Triangle 0");++ // draw incomplete paths+ y_offset += 20;+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_reverse_last ("Point");++ y_offset += 20;+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_reverse_last ("Double point");++ y_offset += 20;+ pen_tool.test_click_action (1, 10 + x_offset, 30 + y_offset);+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_reverse_last ("Vertical line");++ y_offset += 20;+ pen_tool.test_click_action (1, 30 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_reverse_last ("Horisontal line");++ // triangle 1+ y_offset += 20;+ pen_tool.test_click_action (1, 10 + x_offset, -10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (1, 30 + x_offset, 0 + y_offset);+ pen_tool.test_click_action (3, 0, 0);+ test_reverse_last ("Triangle reverse 1");++ // triangle 2+ y_offset += 70;+ pen_tool.test_click_action (1, 40 + x_offset, -50 + y_offset);+ pen_tool.test_click_action (1, 10 + x_offset, -20 + y_offset);+ pen_tool.test_click_action (1, 25 + x_offset, -20 + y_offset); + pen_tool.test_click_action (3, 0, 0);+ test_reverse_last ("Triangle reverse 2");++ // box+ y_offset += 20;+ pen_tool.test_click_action (1, 100 + x_offset, 150 + y_offset);+ pen_tool.test_click_action (1, 150 + x_offset, 150 + y_offset);+ pen_tool.test_click_action (1, 150 + x_offset, 100 + y_offset);+ pen_tool.test_click_action (1, 100 + x_offset, 100 + y_offset); + pen_tool.test_click_action (3, 0, 0); // close+ test_reverse_last ("Box 1");+ + return true;+ }+ + private Tool select_pen () {+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");+ pen_tool.test_select_action ();+ return pen_tool;+ }+ + public bool test_reverse_random_triangles () {+ Tool pen;+ + int ax, bx, cx;+ int ay, by, cy;++ bool r = true;++ test_open_next_glyph ();+ pen = select_pen ();++ int skip = 0;+ for (int i = 0; i < 10; i++) {+ this.yield ();+ + ax = Random.int_range (0, 300);+ bx = Random.int_range (0, 300);+ cx = Random.int_range (0, 300);++ ay = Random.int_range (0, 300);+ by = Random.int_range (0, 300);+ cy = Random.int_range (0, 300);++ pen.test_click_action (1, ax, ay);+ pen.test_click_action (1, bx, by);+ pen.test_click_action (1, cx, cy);+ pen.test_click_action (3, 0, 0);++ // FIXME+ //print (@"TESTING Random triangle ($ax, $by), ($bx, $by), ($cx, $cy)\n");+ + r = test_reverse_last (@"Random triangle № $(i + 1) ($ax, $ay), ($bx, $by), ($cx, $cy)");+ if (!r) {+ test_open_next_glyph ();+ pen = select_pen ();++ pen.test_click_action (1, ax, ay);+ pen.test_click_action (1, bx, by);+ pen.test_click_action (1, cx, cy);+ // pen.test_click_action (3, 0, 0);+ + return false;+ }+ + if (++skip == 5) {+ test_open_next_glyph ();+ skip = 0;+ }+ }+ + if (r) test_open_next_glyph ();+ + return true;+ }+ +}++}new file mode 100644@@ -0,0 +1,119 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class MoveTool : Tool {++ bool move_path = false;++ public MoveTool (string n) {+ base (n, "Move paths", 'm', CTRL);+ + select_action.connect((self) => {+ });++ deselect_action.connect((self) => {+ Glyph glyph = MainWindow.get_current_glyph ();++ foreach (Path p in glyph.path_list) {+ p.set_color (0, 0, 0, 1);+ p.set_selected (false);+ }+ });+ + press_action.connect((self, b, x, y) => {+ Glyph glyph = MainWindow.get_current_glyph ();+ Path path;+ + glyph.store_undo_state ();+ + glyph.move_path_begin (x, y);+ + if (glyph.active_path == null) {+ return;+ }+ + move_path = true;+ + foreach (Path p in glyph.path_list) {+ p.set_color (0, 0, 0, 1);+ p.set_selected (false);+ }+ + return_if_fail (glyph.active_path != null);+ + path = (!) glyph.active_path;+ + if (path.is_clockwise ()) {+ path.set_color (0.2, 0.2, 0.4, 0.8);+ path.set_selected (true);+ } else {+ path.set_color (0.5, 0.5, 0.7, 0.8);+ path.set_selected (true);+ }+ + });++ release_action.connect((self, b, x, y) => {+ Glyph glyph = MainWindow.get_current_glyph ();+ move_path = false;+ + + if (GridTool.is_visible () && glyph.active_path != null) {+ tie_path_to_grid ((!) glyph.active_path, x, y);+ }+ + });+ + move_action.connect ((self, x, y) => {+ if (move_path) {+ Glyph glyph = MainWindow.get_current_glyph ();+ glyph.move_selected_path (x, y);+ MainWindow.get_glyph_canvas ().redraw ();+ }+ });+ }++ void tie_path_to_grid (Path p, double x, double y) {+ double sx, sy, qx, qy; + + // tie to grid+ sx = p.xmax;+ sy = p.ymax;+ qx = p.xmin;+ qy = p.ymin;+ + GridTool.tie_coordinate (ref sx, ref sy);+ GridTool.tie_coordinate (ref qx, ref qy);+ + if (Math.fabs (qy - p.ymin) < Math.fabs (sy - p.ymax)) {+ p.move (0, qy - p.ymin);+ } else {+ p.move (0, sy - p.ymax);+ }++ if (Math.fabs (qx - p.xmin) < Math.fabs (sx - p.xmax)) {+ p.move (qx - p.xmin, 0);+ } else {+ p.move (sx - p.xmax, 0);+ } + }++}++}new file mode 100644@@ -0,0 +1,1098 @@+/* + Copyright (C) 2012 Johan Mattsson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +namespace Supplement { + +class OpenFontFormatWriter : Object { + + DataOutputStream os; + DirectoryTable directory_table; + + public OpenFontFormatWriter () { + directory_table = new DirectoryTable (); + } + + public void open (File file) throws Error { + if (file.query_exists ()) { + throw new FileError.EXIST("OpenFontFormatWriter: file exists."); + } + + os = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION)); + } + + public void add_glyf (Glyf glyf) { + directory_table.get_glyf_table ().add (glyf); + } + + private void add_glyph (Glyph glyph) { + unichar char_code = glyph.get_unichar (); + Glyf g = new Glyf (char_code); + Contour contour; + + foreach (Path p in glyph.path_list) { + contour = new Contour (p); + g.add_contour (contour); + } + + add_glyf (g); + } + + public void write_font_file (Font font) throws Error { + long dl = directory_table.get_font_file_size (); + uint8[] data = new uint8[dl]; + uint i = -1; + long written = 0; + Glyph? g; + unowned List<Table> tables; + unichar indice = 0; + + while (true) { + g = font.get_glyph_indice (indice++); + + if (g == null) { + break; + } + + add_glyph ((!) g); + } + + tables = directory_table.get_tables (); + + foreach (Table t in tables) { + foreach (uint8 d in t.get_font_data ().data) { + data[++i] = d; + } + } + + while (written < data.length) { + written += os.write (data[written:data.length]); + } + } + + public void close () throws Error { + os.close (); + } +} + +class Contour : Object { + + public List<Coordinate> coordinates; + + public Contour (Path path) { + coordinates = new List<Coordinate> (); + set_path (path); + } + + void set_path (Path path) { + Coordinate c; + + if (path.points.length () == 0) { + return; + } + + c = new Coordinate (); + foreach (EditPoint e in path.points) { + c.x = (int16) e.x * 1000; // Fixa: do mapping to em-width and int16.MAX, and use em as 1:1 in canvas + c.y = (int16) e.y * 1000; + c.flag = Coordinate.ON_PATH; + + coordinates.append (c); + } + } + + // TODO: + //void add_phantoms () + + bool is_clockwise (Path path) { + uint len = path.points.length (); + return_val_if_fail (len > 0, false); + + uint i = 0; + unowned List<EditPoint> iter = path.points.first (); + + double xmax = iter.data.x; + double ymax = iter.data.y; + double xmin = iter.data.x; + double ymin = iter.data.y; + + uint i_xmax = i; + uint i_ymax = i; + uint i_xmin = i; + uint i_ymin = i; + + foreach (EditPoint ee in path.points) { + + if (xmax < ee.x) { + xmax = iter.data.x; + i_xmax = i; + } + + if (ymax < ee.y) { + ymax = iter.data.y; + i_ymax = i; + } + + if (xmin > ee.x) { + xmin = iter.data.x; + i_xmin = i; + } + + if (ymin > ee.y) { + ymin = iter.data.y; + i_ymin = i; + } + + i++; + } + + return is_clockwise_extrema (i_xmax, i_ymax, i_xmin, i_ymin); + } + + bool is_clockwise_extrema (uint i_xmax, uint i_ymax, uint i_xmin, uint i_ymin) { + uint t, i; + + for (i = 0; i < 4; i++) { + + if (i_ymin <= i_xmin <= i_ymax <= i_xmax) { + return true; + } + + if (i_ymin <= i_xmax <= i_ymax <= i_xmin) { + return false; + } + + // shift it + t = i_xmin; + i_xmin = i_ymin; + i_ymin = i_xmax; + i_xmax = i_ymax; + i_ymax = t; + } + + warn_if_reached (); + + return true; + } + + public uint16 get_end_point () { + return_val_if_fail (coordinates.length () != 0, 0); + return coordinates.first ().data.y; + } +} + +class Glyf : Object { + + public List<Contour> contours; + + public uint32 char_code; + + public Glyf (unichar character_code) { + char_code = (uint32) character_code; + } + + public uint32 get_char_code () { + return char_code; + } + + public void add_contour (Contour c) { + contours.append (c); + } + + /** Get advance with from lsb to begining of next glyph. */ + public uint16 get_width() { + return 1000; // fixme + } + + /** Get left side bearing, (lsb) */ + public int16 get_left() { + return -1 * (get_width() / 2); + } + + public void get_boundries (out int16 xmin, out int16 ymin, out int16 xmax, out int16 ymax) { + xmin = int16.MAX; + ymin = int16.MAX; + xmax = int16.MIN; + ymax = int16.MIN; + + // Fixme we do need to add phantom points + foreach (var contour in contours) { + foreach (var v in contour.coordinates) { + if (v.x < xmin) xmin = v.x; + if (v.x > xmax) xmax = v.x; + if (v.y < ymin) ymin = v.y; + if (v.y > ymax) ymax = v.y; + } + } + + return; + } + +} + +class FontData : Object { + + public List<uint8> data; + + public FontData () { + } + + public uint length () { + return data.length (); + } + + /** Add additional checksum data to this checksum. */ + public void continous_check_sum (out uint32 current_check_sum) { + uint32 val = 0; + uint8 b = 3; + + current_check_sum = 0; + + if (data.length () % 4 > 0) { + stderr.printf("Warning table data is not padded to correct size.\n"); + } + + foreach (uint8 v in data) { + val += v << b*8; + + if (b == 0) { + current_check_sum += val; + val = 0; + b = 3; + } + + b--; + } + } + + public uint32 check_sum () { + uint32 sum = 0; + continous_check_sum (out sum); + return sum; + } + + public void add (uint8 d) { + data.append (d); + } + + public void add_u16 (uint16 d) { + uint16 n = d >> 8; + add ((uint8)n); + n <<= 8; + add ((uint8)(d - n)); + } + + public void add_16 (int16 i) { + add_u16 (i + (uint16.MAX/2)); + } + + public void add_u32 (uint32 i) { + uint32 s = (uint16) (i >> 16); + + add_u16 ((uint16) s); + add_u16 ((uint16) (i - (s << 16))); + } + + public void add_u64(uint64 i) { + uint64 s = (uint32) (i >> 32); + + add_u32 ((uint32) s); + add_u32 ((uint32)(i - (s << 32))); + } + + public void add_tag (string s) + requires (s.length == 4 && s.data.length == 4) { + + uint8[] data = s.data; + for (int n = 0; n < data.length; n++) { + add (data[n]); + } + } +} + +class Coordinate { + /** TTF coordinate flags. */ + + public static const uint8 ON_PATH = 1 << 0; + public static const uint8 X_SHORT_VECTOR = 1 << 1; + public static const uint8 Y_SHORT_VECTOR = 1 << 2; + public static const uint8 REPEAT = 1 << 3; + + // same flag or short vector sign flag + public static const uint8 X_IS_SAME = 1 << 4; + public static const uint8 Y_IS_SAME = 1 << 5; + public static const uint8 X_SHORT_VECTOR_POSITIVE = 1 << 4; + public static const uint8 Y_SHORT_VECTOR_POSITIVE = 1 << 5; + + public uint8 flag = 0; + public int16 x = 0; + public int16 y = 0; +} + +interface Table : Object { + public abstract string get_id (); + public abstract FontData get_font_data (); +} + +class GlyfTable : Table, Object { + + public List<Glyf> glyfs = new List<Glyf> (); + + FontData? font_data = null; + + int16 xmin = int16.MAX; + int16 ymin = int16.MAX; + int16 xmax = int16.MIN; + int16 ymax = int16.MIN; + + public GlyfTable () { + } + + public void add (Glyf glyf) { + glyfs.insert_sorted_with_data (glyf, (a, b) => { + if (a.char_code < b.char_code) return -1; + if (a.char_code > b.char_code) return 1; + return 0; + }); + } + + public string get_id () { + return "glyf"; + } + + public FontData get_font_data () { + if (font_data != null) return (!) font_data; + + FontData fd = new FontData (); + font_data = fd; + + foreach (var g in glyfs) { + add_glyf_data (g, fd); + } + + // padding + while (fd.length () % 4 != 0) { + fd.add(0); + } + + return fd; + } + + public void get_boundries (out int16 xmin, out int16 ymin, out int16 xmax, out int16 ymax) { + get_font_data(); + + xmin = this.xmin; + ymin = this.ymin; + xmax = this.xmax; + ymax = this.ymax; + } + + private void add_glyf_data (Glyf glyf, FontData fd) { + uint16 n_instructions = 0; + int16 n_contours = (int16) glyf.contours.length(); + + fd.add_16 (n_contours); + + add_boundries (glyf, fd); + + foreach (var contour in glyf.contours) { + fd.add_u16 (contour.get_end_point ()); // endPtsOfContour + } + + fd.add_u16 (n_instructions); // TrueType Instruction reference + + for (uint16 i = 0; i < n_instructions; i++) { + fd.add (0); // instruction + } + + foreach (var contour in glyf.contours) { + add_flags (contour.coordinates, fd); // flags for outline coordinate (flags[n]) + } + + foreach (var contour in glyf.contours) { + add_x_coordinates (contour.coordinates, fd); // X-coordinates + } + + foreach (var contour in glyf.contours) { + add_y_coordinates (contour.coordinates, fd); // Y-coordinates + } + + } + + private void add_boundries (Glyf glyf, FontData fd) { + int16 xmin, ymin, xmax, ymax; + + glyf.get_boundries (out xmin, out ymin, out xmax, out ymax); + + fd.add_16 (xmin); + fd.add_16 (ymin); + fd.add_16 (xmax); + fd.add_16 (xmax); + + if (this.xmin > xmin) this.xmin = xmin; + if (this.ymin > ymin) this.ymin = ymin; + if (this.xmax < xmax) this.xmax = xmax; + if (this.ymax < ymax) this.ymax = ymax; + + } + + private void add_flags (List<Coordinate> coordinates, FontData fd) { + foreach (var v in coordinates) { + fd.add (v.flag); + } + } + + private static bool in_flag (uint8 val, uint8 flag) { + return (val & flag) == val; + } + + private void add_x_coordinates (List<Coordinate> coordinates, FontData fd) { + int16 last = 0; + foreach (var v in coordinates) { + if (in_flag (Coordinate.X_SHORT_VECTOR, v.flag)) { + fd.add ((uint8) (v.x - last)); + } else { + fd.add_16 (v.x - last); + } + last = v.x; + } + } + + private void add_y_coordinates (List<Coordinate> coordinates, FontData fd) { + int16 last = 0; + foreach (var v in coordinates) { + if (in_flag (Coordinate.Y_SHORT_VECTOR, v.flag)) { + fd.add ((uint8)(v.y - last)); + } else { + fd.add_16 (v.y - last); + } + last = v.y; + } + } + +} + +class CmapSubtable { + public static const uint16 FORMAT_TRIMMED_ARRAY = 10; + + public uint32 start_char_code = 0; + public uint32 num_chars = 0; +} + +class CmapTable : Table, Object { + + GlyfTable glyf_table; + FontData? table_data; + + public CmapTable(GlyfTable gt) { + glyf_table = gt; + table_data = null; + } + + public string get_id () { + return "cmap"; + } + + /** Character to glyph mapping */ + public FontData get_font_data () { + if (table_data != null) return (!) table_data; + + FontData fd = new FontData (); + table_data = fd; + + List<CmapSubtable> subtables = cmap_sub_tables (); + uint16 n_encoding_tables = (uint16) subtables.length (); + uint32 offset; + + uint16 glyph_indice = 0; + + fd.add_u16 (0); // table version + fd.add_u16 (n_encoding_tables); + + fd.add_u16 (3); // platform + fd.add_u16 (10); // Format Unicode UCS-4 + + offset = fd.data.length () + 4; + + fd.add_u32 (offset); + + foreach (CmapSubtable cmap in subtables) { + uint32 num_chars = cmap.num_chars; + uint32 table_length = 20 + 2 * num_chars; // of subtable + uint32 language = 0; + uint32 start_char_code = cmap.start_char_code; + + // Subtable + fd.add_u16 (cmap.FORMAT_TRIMMED_ARRAY); // Format for this subtable + fd.add_u16 (0); // Reserved + + fd.add_u32 (table_length); + fd.add_u32 (language); + fd.add_u32 (start_char_code); + fd.add_u32 (num_chars); + + for (int i = 0; i < num_chars; i++) { + fd.add_u16 (glyph_indice++); + } + } + + // padding + while (fd.length () % 4 != 0) { + fd.add(0); + } + + + return fd; + } + + List<CmapSubtable> cmap_sub_tables () { + List<CmapSubtable> maps = new List<CmapSubtable> (); + + CmapSubtable cmap = new CmapSubtable (); + + uint32 charcode = 0; + uint32 last = 0; + foreach (Glyf g in glyf_table.glyfs) { + charcode = g.get_char_code (); + + if (charcode != last + 1 || last == 0) { + cmap = new CmapSubtable (); + cmap.start_char_code = charcode; + + maps.append(cmap); + } + + last = charcode; + cmap.num_chars++; + + assert(maps.length () > 0); + } + + return maps; + } + +} + +class HeadTable : Table, Object { + + uint32 check_sum_adjustment = 0; + + GlyfTable glyf_table; + + public HeadTable (GlyfTable gt) { + glyf_table = gt; + } + + public string get_id () { + return "head"; + } + + public void set_check_sum_adjustment (uint32 csa) { + check_sum_adjustment = csa; + } + + public FontData get_font_data () { + + int16 xmin, ymin, xmax, ymax; + + FontData font_data = new FontData (); + + font_data.add_u16 (1); // table version + font_data.add_u16 (0); + + // font revision + font_data.add_u16 (0); + font_data.add_u16 (0); + + // Zero on the first run and updated by directory tables checksum calculation + // for the entire font. + font_data.add_u32 (check_sum_adjustment); // TODO + + font_data.add_u32 (0x5F0F3CF5); // magic number + + font_data.add_u16 (0); // clear flags + + font_data.add_u16 (2048); // units per em (in power of two for ttf) + + font_data.add_u64 (0); //creation time since 1904-01-01 + font_data.add_u64 (0); //modified time since 1904-01-01 + + glyf_table.get_boundries(out xmin, out ymin, out xmax, out ymax); + + font_data.add_16 (xmin); + font_data.add_16 (ymin); + font_data.add_16 (xmax); + font_data.add_16 (ymax); + + font_data.add_u16 (0); // mac style + font_data.add_u16 (0); // smallest recommended size in pixels + font_data.add_u16 (2); // Deprecated direction hint + font_data.add_16 (1); // Long offset + font_data.add_16 (0); // Use current glyph data format + + // padding + while (font_data.length () % 4 != 0) { + font_data.add(0); + } + + return font_data; + } +} + +class HheaTable : Table, Object { + + GlyfTable glyf_table; + + public HheaTable (GlyfTable g) { + glyf_table = g; + } + + public string get_id () { + return "head"; + } + + public FontData get_font_data () { + FontData font_data = new FontData (); + + font_data.add_u16 (1); // table version + font_data.add_u16 (0); + + font_data.add_16 (0); // Ascender Typographic ascent. + font_data.add_16 (0); // Descender + font_data.add_16 (0); // LineGap + + font_data.add_u16 (0); // advanceWidthMax Maximum advance width value in 'hmtx' table. + + font_data.add_16 (0); // minLeftSideBearing + font_data.add_16 (0); // minRightSideBearing + font_data.add_16 (0); // xMaxExtent Max(lsb + (xMax - xMin)) + + font_data.add_16 (0); // caretSlopeRise + font_data.add_16 (0); // caretSlopeRun + font_data.add_16 (0); // caretOffset + + // reserved + font_data.add_16 (0); + font_data.add_16 (0); + font_data.add_16 (0); + font_data.add_16 (0); + + font_data.add_16 (0); // metricDataFormat 0 for current format. + + font_data.add_u16 ((uint16)glyf_table.glyfs.length()); // numberOfHMetrics Number of hMetric entries in 'hmtx' table + + // hmtx - Horizontal metrix (Own table?) + foreach (var g in glyf_table.glyfs) { + font_data.add_u16 (g.get_width()); + font_data.add_16 (g.get_left()); + } + + // hmtx for monospaced glyphs + // ... none right now + + // padding + while (font_data.length () % 4 != 0) { + font_data.add(0); + } + + return font_data; + } +} + +class MaxpTable : Table, Object { + + GlyfTable glyf_table; + + public MaxpTable (GlyfTable g) { + glyf_table = g; + } + + public string get_id () { + return "maxp"; + } + + public FontData get_font_data () { + FontData font_data = new FontData(); + + // Version 0.5 for fonts with cff data and 1.0 for ttf + font_data.add_u16 (1); + font_data.add_u16 (0); + + uint16 max_points, max_contours; + + get_max (out max_points, out max_contours); + + font_data.add_u16 ((uint16)glyf_table.glyfs.length ()); // numGlyphs in the font + font_data.add_u16 (max_points); + font_data.add_u16 (max_contours); // maxContours Maximum contours in a non-composite glyph. + font_data.add_u16 (0); // maxCompositePoints Maximum points in a composite glyph. + font_data.add_u16 (0); // maxCompositeContours Maximum contours in a composite glyph. + font_data.add_u16 (2); // maxZones 1 if instructions do not use the twilight zone (Z0), or 2 if instructions do use Z0; should be set to 2 in most cases. + font_data.add_u16 (0); // maxTwilightPoints Maximum points used in Z0. + font_data.add_u16 (0); // maxStorage Number of Storage Area locations. + font_data.add_u16 (0); // maxFunctionDefs Number of FDEFs. + font_data.add_u16 (0); // maxInstructionDefs Number of IDEFs. + font_data.add_u16 (0); // maxStackElements Maximum stack depth2. + font_data.add_u16 (0); // maxSizeOfInstructions Maximum byte count for glyph instructions. + font_data.add_u16 (0); // maxComponentElements Maximum number of components referenced at "top level" for any + font_data.add_u16 (0); // maxComponentDepth Maximum levels of recursion; 1 for simple components. + + // padding + while (font_data.length () % 4 != 0) { + font_data.add(0); + } + + return font_data; + } + + private void get_max (out uint16 max_points, out uint16 max_contours) { + max_points = 0; + max_contours = 0; + + foreach (var g in glyf_table.glyfs) { + uint16 p = 0; + + if (g.contours.length() > max_contours) { + max_contours = (uint16) g.contours.length(); + } + + foreach (var c in g.contours) { + p += (uint16) c.coordinates.length(); + } + + if (p > max_points) max_points = p; + } + + } + +} + +class OffsetTable : Table, Object { + uint16 num_tables; + + public OffsetTable () { + num_tables = 0; + } + + public void set_num_tables (uint n) { + num_tables = (uint16) n; + } + + public string get_id () { + // warn_if_reached (); + return "Offset table"; + } + + public FontData get_font_data () { + FontData fd = new FontData (); + + uint16 search_range = 0; + uint16 entry_selector = 0; + uint16 range_shift = 0; + + // version 1.0 for TTF CFF else use OTTO + fd.add_u16 (1); + fd.add_u16 (0); + + fd.add_u16 (num_tables); + fd.add_u16 (search_range); + fd.add_u16 (entry_selector); + fd.add_u16 (range_shift); + + // padding + while (fd.length () % 4 != 0) { + fd.add(0); + } + + return fd; + } +} + +class NameTable : Table, Object { + + public NameTable () { + } + + public string get_id () { + return "name"; + } + + public FontData get_font_data () { + // TODO: Apple table versioning of font data + + FontData font_data = new FontData (); + + font_data.add_u16 (0); // Format selector + font_data.add_u16 (0); // Number of name records. + font_data.add_u16 (6); // Offset to start of string storage (from start of table). + + // ... + + // padding + while (font_data.length () % 4 != 0) { + font_data.add(0); + } + + return font_data; + } + +} + +class Os2Table : Table, Object { + + public Os2Table () { + } + + public string get_id () { + return "OS/2"; + } + + public FontData get_font_data () { + FontData font_data = new FontData (); + + font_data.add_u16 (4); // USHORT Version 0x0000, 0x0001, 0x0002, 0x0003, 0x0004 + + font_data.add_16 (0); // SHORT xAvgCharWidth + + font_data.add_u16 (0); // USHORT usWeightClass + font_data.add_u16 (0); // USHORT usWidthClass + font_data.add_u16 (0); // USHORT fsType + + font_data.add_16 (0); // SHORT ySubscriptXSize + font_data.add_16 (0); // SHORT ySubscriptYSize + font_data.add_16 (0); // SHORT ySubscriptXOffset + font_data.add_16 (0); // SHORT ySubscriptYOffset + font_data.add_16 (0); // SHORT ySuperscriptXSize + font_data.add_16 (0); // SHORT ySuperscriptYSize + font_data.add_16 (0); // SHORT ySuperscriptXOffset + font_data.add_16 (0); // SHORT ySuperscriptYOffset + font_data.add_16 (0); // SHORT yStrikeoutSize + font_data.add_16 (0); // SHORT yStrikeoutPosition + font_data.add_16 (0); // SHORT sFamilyClass + + // PANOSE + font_data.add (0); + font_data.add (0); + font_data.add (0); + font_data.add (0); + font_data.add (0); + font_data.add (0); + font_data.add (0); + font_data.add (0); + font_data.add (0); + font_data.add (0); + + font_data.add_u32 (0); // ulUnicodeRange1 Bits 0-31 + font_data.add_u32 (0); // ULONG ulUnicodeRange2 Bits 32-63 + font_data.add_u32 (0); // ULONG ulUnicodeRange3 Bits 64-95 + font_data.add_u32 (0); // ULONG ulUnicodeRange4 Bits 96-127 + + font_data.add_tag ("----"); // VendID + + font_data.add_u16 (0); // USHORT fsSelection + font_data.add_u16 (0); // USHORT usFirstCharIndex + font_data.add_u16 (0); // USHORT usLastCharIndex + + font_data.add_16 (0); // SHORT sTypoAscender + font_data.add_16 (0); // SHORT sTypoDescender + font_data.add_16 (0); // SHORT sTypoLineGap + + font_data.add_u16 (0); // USHORT usWinAscent + font_data.add_u16 (0); // USHORT usWinDescent + + font_data.add_u32 (0); // ULONG ulCodePageRange1 Bits 0-31 + font_data.add_u32 (0); // ULONG ulCodePageRange2 Bits 32-63 + + font_data.add_16 (0); // SHORT sxHeight version 0x0002 and later + font_data.add_16 (0); // SHORT sCapHeight version 0x0002 and later + + font_data.add_16 (0); // USHORT usDefaultChar version 0x0002 and later + font_data.add_16 (0); // USHORT usBreakChar version 0x0002 and later + font_data.add_16 (0); // USHORT usMaxContext version 0x0002 and later + + // padding + while (font_data.length () % 4 != 0) { + font_data.add(0); + } + + return font_data; + } + +} + +class PostTable : Table, Object { + + public PostTable () { + } + + public string get_id () { + return "post"; + } + + public FontData get_font_data () { + FontData font_data = new FontData (); + + // Version + font_data.add_u16 (3); + font_data.add_u16 (0); + + // italicAngle + font_data.add_u16 (0); + font_data.add_u16 (0); + + font_data.add_16 (-2); // underlinePosition + font_data.add_16 (1); // underlineThickness + + font_data.add_u32 (0); // non zero for monospaced font + + // mem boundries may be omitted + font_data.add_u32 (0); // min mem + font_data.add_u32 (0); // max mem + + font_data.add_u32 (0); // min mem for Type1 + font_data.add_u32 (0); // max mem for Type1 + + // padding + while (font_data.length () % 4 != 0) { + font_data.add(0); + } + + return font_data; + } + +} + +/** Table with list of tables sorted by table tag. */ +class DirectoryTable : Table, Object { + + CmapTable cmap_table; + GlyfTable glyf_table; + HeadTable head_table; + HheaTable hhea_table; + MaxpTable maxp_table; + NameTable name_table; + Os2Table os_2_table; + PostTable post_table; + + OffsetTable offset_table; + + List<Table> tables; + + public DirectoryTable () { + + offset_table = new OffsetTable (); + + glyf_table = new GlyfTable (); + cmap_table = new CmapTable (glyf_table); + head_table = new HeadTable (glyf_table); + hhea_table = new HheaTable (glyf_table); + maxp_table = new MaxpTable (glyf_table); + name_table = new NameTable (); + os_2_table = new Os2Table (); + post_table = new PostTable (); + + tables = new List<Table> (); + + tables.append (offset_table); // The the directory index tables + tables.append (this); + + tables.append (cmap_table); // The other required tables + tables.append (glyf_table); + tables.append (head_table); + tables.append (hhea_table); + tables.append (maxp_table); + tables.append (name_table); + tables.append (os_2_table); + tables.append (post_table); + + offset_table.set_num_tables (tables.length () - 2); // number of tables, skip DirectoryTable and OffsetTable + } + + public string get_id () { + return "Directory table"; // Table id should be ignored for directory table, none the less it has one declared here. + } + + public unowned List<Table> get_tables () { + return tables; + } + + public GlyfTable get_glyf_table () { + return glyf_table; + } + + public long get_font_file_size () { + long length = offset_table.get_font_data ().length (); + + foreach (Table t in tables) { + length += t.get_font_data ().length (); + } + + return length; + } + + public FontData get_font_data () { + FontData fd = new FontData (); + + uint32 table_offset = 0; + uint32 table_length = 0; + + uint32 check_sum = 0; + + table_offset += offset_table.get_font_data ().length (); + + head_table.set_check_sum_adjustment (0); + + foreach (Table t in tables) { + + if (t is DirectoryTable || t is OffsetTable) { + continue; + } + + table_length = t.get_font_data ().length (); + + fd.add_tag (t.get_id ()); // name of table + fd.add_u32 (t.get_font_data ().check_sum ()); + fd.add_u32 (table_offset); + fd.add_u32 (table_length); + + table_offset += table_length; + } + + // padding + while (fd.length () % 4 != 0) { + fd.add(0); + } + + // Check sum adjustment for the entire font + foreach (Table t in tables) { + + if (t is DirectoryTable) { + fd.continous_check_sum (out check_sum); + continue; + } + + t.get_font_data ().continous_check_sum (out check_sum); + } + + head_table.set_check_sum_adjustment ((uint32)(0xB1B0AFBA - check_sum)); + + return fd; + } + +} + + +} new file mode 100644@@ -0,0 +1,827 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;++namespace Supplement {++/** A display with all glyphs present in this font. */+class OverView : FontDisplay {+ Allocation allocation;+ + int rows = 0;+ int items_per_row = 0;+ unichar first_character = 1;+ unichar first_visible = 1;+ unichar selected = 1;+ + double view_offset_y = 0;+ double view_offset_x = 0;+ + int nail_height = 0;+ int nail_width = 0;++ double nail_zoom_height = 0;+ double nail_zoom_width = 0;+ + public signal void open_glyph_signal (string c);++ GlyphRange glyph_range;++ List<GlyphCollection> visible_characters = new List<GlyphCollection> ();++ List<unichar> zoom_list = new List<unichar> ();+ int zoom_list_index = 0;++ List<GlyphCollection> deleted_glyphs = new List<GlyphCollection> ();++ public OverView (GlyphRange? range = null) {+ if (range == null) {+ glyph_range = new GlyphRange ();+ glyph_range.use_default_range ();+ }+ + reset_zoom ();++ this.open_glyph_signal.connect ((t, s) => {+ TabBar tabs = MainWindow.get_tab_bar ();+ Toolbox tools = MainWindow.get_toolbox ();+ bool selected = tabs.select_char (s);+ unichar new_char = s.get_char (0);+ + stdout.printf ("Open '%s' character: %u (%s)\n", s, new_char, Font.to_hex (new_char));+ + if (!selected) {+ Font f = Supplement.get_current_font ();+ GlyphCollection? fg = f.get_glyph_collection (s);+ Glyph g = (fg == null) ? new Glyph (s, new_char) : ((!) fg).get_current ();+ ZoomTool z = (ZoomTool) tools.get_tool ("zoom_tool");+ + z.store_current_view ();+ f.add_glyph (g);+ tabs.add_tab (g);+ + MainWindow.get_glyph_canvas ().set_current_glyph (g);+ + g.default_zoom ();+ z.store_current_view ();+ }+ });++ }++ public override void scroll_wheel_up (Gdk.EventScroll e) {+ key_up ();+ close_menus ();+ redraw_area (0, 0, allocation.width, allocation.height);+ }+ + public override void scroll_wheel_down (Gdk.EventScroll e) {+ key_down ();+ close_menus ();+ redraw_area (0, 0, allocation.width, allocation.height);+ }+ + public override void selected_canvas () {+ redraw_area (0, 0, allocation.width, allocation.height);+ KeyBindings.singleton.set_require_modifier (true);+ }+ + public override void zoom_min () {+ int minw = 26 * 2;+ int minh = 46 * 2;+ + if (zoom_value_in_range (minw, minh)) {+ nail_width = minw;+ nail_height = minh;+ + nail_zoom_height = minw; + nail_zoom_width = minh; + + adjust_scroll ();+ redraw_area (0, 0, allocation.width, allocation.height);+ } else {+ warn_if_reached ();+ } + }+ + public override void reset_zoom () {+ nail_height = 210;+ nail_width = 150;+ + nail_zoom_height = nail_height; + nail_zoom_width = nail_width;+ + adjust_scroll ();+ redraw_area (0, 0, allocation.width, allocation.height);+ }++ public override void zoom_max () {+ if (zoom_value_in_range (allocation.width, allocation.height)) {+ nail_width = allocation.width;+ nail_height = allocation.height;+ + nail_zoom_height = allocation.height; + nail_zoom_width = allocation.width; + + adjust_scroll ();+ redraw_area (0, 0, allocation.width, allocation.height);+ } else {+ warn_if_reached ();+ }+ }+ + public override void zoom_in () {+ double w = (1.1 * nail_width);+ double h = (1.1 * nail_height);+ int nw = (int) w;+ int nh = (int) h;+ + if (zoom_value_in_range (nw, nh)) {+ nail_width = nw;+ nail_height = nh;+ + nail_zoom_height = h; + nail_zoom_width = w;+ + adjust_scroll ();+ redraw_area (0, 0, allocation.width, allocation.height);+ }+ }+ + public override void zoom_out () {+ double w = (nail_width / 1.1);+ double h = (nail_height / 1.1);+ int nw = (int) w;+ int nh = (int) h;+ + if (zoom_value_in_range (nw, nh)) {+ nail_width = nw;+ nail_height = nh;++ nail_zoom_height = h; + nail_zoom_width = w;+ + adjust_scroll (); + redraw_area (0, 0, allocation.width, allocation.height);+ }+ }+ + private bool zoom_value_in_range (int w, int h) {+ return (25 < w <= allocation.width && 45 < h <= allocation.height);+ }++ public override void store_current_view () {+ if (zoom_list_index + 1 < zoom_list.length ()) {+ unowned List<unichar> n = zoom_list.nth (zoom_list_index);+ while (n != zoom_list.last ()) zoom_list.delete_link (zoom_list.last ());+ }+ + zoom_list.append (selected);+ zoom_list_index = (int) zoom_list.length () - 1;+ + if (zoom_list.length () > 50) zoom_list.delete_link (zoom_list.first ());+ }+ + public override void restore_last_view () + requires (zoom_list.length () > 0)+ { + if (zoom_list_index - 1 < 0 || zoom_list.length () == 0)+ return;+ + zoom_list_index--;+ }++ public override void next_view () + requires (zoom_list.length () > 0)+ {+ if (zoom_list_index + 1 >= zoom_list.length ())+ return;+ + zoom_list_index++;+ }++ + public override string get_name () {+ return "Overview";+ }+ + private unowned List<GlyphCollection> get_visible_glyphs () {+ return visible_characters;+ }+ + public void draw_caption (int row, double width, Context cr, double y, unichar index_begin) {+ string character_string;+ double left_margin, x, caption_y; // for glyph caption+ unichar index = index_begin;+ int i = row * items_per_row;+ cr.save ();+ cr.set_line_width (1);+ + x = 20;+ + cr.set_font_size (14);++ left_margin = 0;+ + if (nail_width >= 70)+ left_margin = 14;+ else if (nail_width >= 60)+ left_margin = 9;+ else if (nail_width >= 36)+ left_margin = -2;+ else if (nail_width >= 26)+ left_margin = -8;+ else+ warning ("Max zoom level reached");+ + while (x < width) {+ if (index > glyph_range.get_length ()) {+ break;+ }+ + character_string = glyph_range.get_char (index);+ + cr.save ();+ draw_thumbnail (cr, character_string, x, y);+ cr.restore ();++ cr.save ();+ + if (index == selected) {+ cr.set_source_rgba (142/255.0, 158/255.0, 190/255.0, 1);+ } else {+ cr.set_source_rgba (164/255.0, 185/255.0, 215/255.0, 1);+ }+ + cr.set_line_join (LineJoin.ROUND);+ cr.set_line_width(12);+ + cr.rectangle (x, y + nail_height - 20, nail_width - 30, 10); + cr.stroke ();+ cr.restore ();+ + draw_menu (cr, character_string, x, y);+ + caption_y = y + 10 + nail_height - 20;+ + cr.move_to (x, caption_y);+ cr.show_text (character_string);+ + x += nail_width;+ index++;+ i++;+ }+ + cr.stroke();+ }+ + private void draw_menu (Context cr, string name, double x, double y) {+ GlyphCollection? gl = Supplement.get_current_font ().get_glyph_collection (name);+ GlyphCollection g;+ DropMenu menu;+ + if (gl == null) {+ return;+ }+ + g = (!) gl;+ + menu = g.get_version_list ();+ menu.set_position (x + nail_width - 40, y + nail_height - 21);+ menu.draw_icon (cr);+ menu.draw_menu (cr);+ + visible_characters.append (g);+ }+ + private bool draw_thumbnail (Context cr, string name, double x, double y) {+ Glyph? gl = Supplement.get_current_font ().get_glyph (name);+ Glyph g;+ double gx, gy;+ + double x1, x2, y1, y2;+ + double scale = nail_zoom_width / 150.0;+ + if (gl == null) {+ return false;+ }+ + g = (!) gl;+ + g.boundries (out x1, out y1, out x2, out y2);+ + /*+ if (x1 < -8500) return false; + if (x2 > 8500) return false;+ if (y1 < -7600) return false;+ if (y2 > 7600) return false;+ */+ + gx = x + nail_width / 2.0 - g.get_width () / 2.0;+ gy = y + nail_height;+ + gx /= scale;+ gy /= scale;++ gx -= 20;+ gy -= 80;+ + Svg.draw_svg_path (cr, g.get_svg_data (), gx, gy, scale);++ return true;+ }++ // 150 * 190 px+ public override void draw (Allocation allocation, Context cr) {+ double width;+ double y;+ unichar t;+ int i;++ while (visible_characters.length () > 9) {+ visible_characters.remove_link (visible_characters.first ());+ }++ this.allocation = allocation;+ + cr.save ();+ cr.set_source_rgba (1, 1, 1, 1);+ cr.rectangle (0, 0, allocation.width, allocation.height + nail_height);+ cr.fill ();+ cr.restore ();+ + int n_items = (allocation.width / nail_width);+ rows = (allocation.height / nail_height);+ + if (items_per_row != n_items) {+ + if (items_per_row == 0) {+ items_per_row = n_items;+ first_visible = first_character;+ } else {+ items_per_row = n_items;+ first_visible = first_character;+ + while (selected < first_visible)+ scroll (-nail_height);+ + while (selected < first_visible + rows * n_items)+ scroll (nail_height);+ + }+ + scroll_top ();+ + while (first_visible <= selected) {+ scroll_rows (1);+ }+ scroll_rows (-2);+ }++ width = nail_width * items_per_row;+ view_offset_x = (allocation.width - width) / 2.0;+ + cr.translate(view_offset_x, view_offset_y);+ + y = 0;+ t = first_visible;+ i = 0;+ while (y < allocation.height + nail_height) {+ draw_caption (i, width, cr, y, t);++ t += (allocation.width / nail_width);+ y += nail_height;+ i++;+ }+ + draw_menus (allocation, cr);+ }++ private void draw_menus (Allocation allocation, Context cr) {+ }++ private void close_menus () {+ }+ + public void scroll_rows (int row_adjustment) {+ for (int i = 0; i < row_adjustment; i++) {+ scroll (-nail_height);+ }+ + for (int i = 0; i > row_adjustment; i--) {+ scroll (nail_height);+ }+ }+ + public void scroll (int pixel_adjustment) {+ view_offset_y += pixel_adjustment;+ + if (view_offset_y >= 0) { + view_offset_y = -nail_height;+ first_visible -= items_per_row;+ }+ + if (view_offset_y < -nail_height) {+ view_offset_y = 0;+ first_visible += items_per_row;+ }+ }+ + public void scroll_top () {+ first_visible = first_character;+ view_offset_y = 0;+ }+ + private int selected_offset_y (unichar item) {+ if (unlikely (items_per_row == 0)) return 0;+ return (int) (view_offset_y + (((item - first_visible) / items_per_row) * nail_height));+ }++ /** Make selected item fully visible if it partly off screen. */+ private void adjust_scroll () {+ int off = selected_offset_y (selected);++ if (off < 0) {+ close_menus ();+ scroll (-1 * off);+ } else if (off > allocation.height - nail_height) {+ scroll ((allocation.height - nail_height) - off);+ }+ }++ private void key_down () {+ if (glyph_range.length () == 0) return;+ + if (at_bottom ()) {+ int len = (int) glyph_range.get_length ();+ int s = (int) selected;+ + if (len - items_per_row >= s) {+ selected += items_per_row; + } else if (s < len) {+ selected++;+ }+ + if (selected > len) {+ selected = len;+ }+ + adjust_scroll ();+ + return;+ }+ + selected += items_per_row;+ + if (selected > rows * items_per_row + first_visible) {+ scroll (-nail_height);+ }+ + adjust_scroll ();+ }++ private void key_up () {+ + if (glyph_range.length () == 0) return;+ + if (selected < first_character + items_per_row) {+ return;+ }+ + if (selected - items_per_row >= first_character) {+ selected -= items_per_row;+ if (selected < first_visible) {+ scroll (-nail_height);+ }+ } + + if (first_visible < first_character) {+ selected = first_character;+ }+ + adjust_scroll ();+ } + + public void open_current_glyph () {+ EventKey e = { 0 };+ e.type = EventType.KEY_PRESS;+ e.keyval = Key.ENTER;+ key_press (e);+ }++ public void select_next_glyph () {+ EventKey e = { 0 };+ e.type = EventType.KEY_PRESS;+ e.keyval = Key.RIGHT;+ key_press (e);+ }++ public void key_right () { + int len = (int) glyph_range.get_length ();+ + if (len == 0) return;+ + selected++;+ + if (selected > len) {+ selected = len;+ }++ if (selected_offset_y (selected) > allocation.height - nail_height) {+ selected -= items_per_row;+ key_down ();+ } + }+ + public void key_left () {+ if (selected > first_character && glyph_range.length () != 0) {+ selected--; + if (selected_offset_y (selected) < 0) {+ selected -= items_per_row;+ key_up ();+ }+ }+ }+ + public string get_selected_char () {+ return glyph_range.get_char (selected);+ }+ + public override void key_press (EventKey e) {+ redraw_area (0, 0, allocation.width, allocation.height);+ close_menus ();+ + if (e.type != EventType.KEY_PRESS) {+ return;+ }++ if (KeyBindings.modifier == CTRL) {+ return;+ }++ switch (e.keyval) {+ case Key.ENTER:+ open_glyph_signal (get_selected_char ());+ return;+ + case Key.UP:+ key_up ();+ return;+ + case Key.RIGHT:+ key_right ();+ return;+ + case Key.LEFT:+ key_left ();+ return;+ + case Key.DOWN:+ key_down ();+ return;+ + case Key.PG_UP:+ for (int i = 0; i++ < rows; i++) {+ key_up ();+ }+ return;+ + case Key.PG_DOWN:+ for (int i = 0; i++ < rows; i++) {+ key_down ();+ }+ return;+ + case Key.DEL:+ delete_selected_glyph ();+ return;+ }++ scroll_to_char (e.keyval);+ }++ public void delete_selected_glyph () {+ delete_glyph (glyph_range.get_char (selected));+ }+ + public void delete_glyph (string g) {+ GlyphCollection? gc;+ Font f = Supplement.get_current_font ();+ + gc = f.get_glyph_collection (g);++ if (gc != null) {+ deleted_glyphs.append ((!) gc);+ }++ select_next_glyph ();+ f.delete_glyph (g);+ MainWindow.get_tab_bar ().close_by_name (g);+ set_glyph_range (glyph_range);+ f.touch ();+ }+ + public override void undo () {+ Font f = Supplement.get_current_font ();+ + if (deleted_glyphs.length () == 0) {+ return;+ }+ + f.add_glyph_collection (deleted_glyphs.last ().data);+ deleted_glyphs.remove_link (deleted_glyphs.last ());+ }+ + public void scroll_to_char (unichar c) {+ GlyphRange gr = glyph_range;+ GlyphRange full = new GlyphRange ();+ + if (is_modifier_key (c)) {+ return;+ }+ + full.use_full_unicode_range ();+ + set_glyph_range (full);+ selected = c + 1;+ set_glyph_range (gr);+ + }+ + public override void motion_notify (EventMotion e) {+ }+ + public override void button_release (EventButton event) {+ }+ + public override void leave_notify (EventCrossing e) {+ }+ + private void selection_click (EventButton e) {+ double x = view_offset_x;+ double y = view_offset_y;++ unichar current = selected;++ // click in margin+ int n_items = (allocation.width / nail_width);+ double m = (allocation.width - (n_items * nail_width)) / 2.0;+ + bool menu_action = false;+ + foreach (GlyphCollection g in get_visible_glyphs ()) {+ bool a;++ a = g.get_version_list ().menu_item_action (e.x - view_offset_x, e.y - view_offset_y); // select one item on the menu+ + if (a) {+ return;+ }+ + a = g.get_version_list ().menu_icon_action (e.x - view_offset_x, e.y - view_offset_y); // open menu+ + if (a) {+ menu_action = true;+ }+ }+ + if (e.x < m) {+ e.x = m + 1;+ }+ + if (e.x > allocation.width - m) {+ e.x = allocation.width - m - 1;+ }+ + if (e.y < 5) {+ e.y = 5;+ }+ + if (e.y > allocation.height - 5) {+ e.y = allocation.height - 5;+ }+ + // empty set+ if (glyph_range.length () == 0) return;++ // scroll to the selected glyph+ selected = first_visible;++ while (y < e.y < allocation.height) {+ selected += items_per_row;+ y += nail_height;+ }+ selected -= items_per_row;++ while (x < e.x < allocation.width) {+ selected++;+ x += nail_width;+ }+ selected--;+ + if (current == selected && !menu_action) {+ open_glyph_signal (glyph_range.get_char (selected));+ }+ + adjust_scroll ();+ adjust_scroll ();+ + redraw_area (0, 0, allocation.width, allocation.height);+ }+ + public override void button_press (EventButton e) {+ selection_click (e);+ }++ /** Returns true if overview shows the last character. */+ private bool at_bottom () {+ return (rows * items_per_row + items_per_row + first_visible > glyph_range.length ());+ }++ public void set_glyph_range (GlyphRange range) {+ string c = glyph_range.get_char (selected); + bool done = false;+ unichar i;+ unichar len;+ glyph_range = range;+ + // scroll to the selected character+ if (c == "") {+ print (@"No glyph.. \"$c\" move to a\n");+ c = "a";+ }+ + if (items_per_row == 0) {+ return;+ }+ + scroll_top ();++ while (true) {+ for (i = first_visible; i < first_visible + items_per_row * (rows + 1); i++) {+ if (range.get_char (i) == c) {+ selected = i;+ done = true;+ break;+ }+ }+ + if (done) break;+ if (at_bottom ()) break;+ + scroll_rows (rows);+ }++ if (unlikely (!done)) {+ print (@"No glyph.. \"$c\"\n");+ selected = 0;+ }+ + //scroll_rows (-1);+ adjust_scroll ();+ + if (at_bottom ()) {+ while (c != glyph_range.get_char (selected) && glyph_range.get_length () > items_per_row) {+ key_right (); + if (selected > glyph_range.get_length () - items_per_row) {+ break;+ }+ }++ len = glyph_range.get_length ();+ for (i = selected; i < len; i++) {+ if (c == glyph_range.get_char (i)) {+ break;+ }+ }++ if (i == len) {+ selected = first_character;+ scroll_top ();+ }+ }+ + redraw_area (0, 0, allocation.width, allocation.height);+ }++}++}new file mode 100644@@ -0,0 +1,1659 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {++enum Direction {+ CLOCKWISE,+ COUNTER_CLOCKWISE+}++class Path {+ + public List<EditPoint> points = new List<EditPoint> ();++ EditPoint? last_point = null;+ EditPoint? second_last_point = null;++ /** Region boundries */+ public double xmax = double.MIN;+ public double xmin = double.MAX;+ public double ymax = double.MIN;+ public double ymin = double.MAX;++ bool edit = true;+ bool open = true;++ bool no_derived_direction = false;+ bool clockwise_direction = true;++ delegate bool RasterIterator (double x, double y); // iterate over each pixel at a given zoom level++ public double r = 0;+ public double g = 0;+ public double b = 0;+ public double a = 1;++ private string? name = null;+ + bool selected = false;+ + public Path () {+ }++ public void set_color (double r, double g, double b, double a) {+ this.r = r;+ this.g = g;+ this.b = b;+ this.a = a;+ }++ public void set_name (string n) {+ name = n;+ }+ + public string get_name () {+ return (name == null) ? "NO_NAME" : (!) name;+ }++ public double get_width () {+ return_if_fail (xmax != double.MIN && xmin != double.MAX);+ return (xmax - xmin);+ }++ public bool empty () {+ return points.length () == 0;+ }++ public void set_selected (bool s) {+ selected = s;+ }++ public bool is_selected () {+ return selected;+ }+ + public void draw_edit_points (Context cr, Allocation allocation, double view_zoom) {+ unowned List<EditPoint> ep = points;+ + unowned EditPoint? n = null;+ unowned EditPoint en;+ unowned EditPoint em;+ + cr.new_path ();+ + // draw lines+ foreach (EditPoint e in ep) {+ if (n != null) {+ en = (!) n;+ draw_next (e, en, cr);+ }+ + n = e;+ }+ + // close path+ if (!is_open () && ep.length () >= 2) {+ en = (!)n;+ em = ep.first ().data;+ + draw_next (em, en, cr);+ }++ if (is_selected ()) {+ // fill path+ cr.close_path ();+ cr.set_source_rgba (r, g, b, a);+ cr.fill ();+ }++ cr.stroke ();+ + // control points for curvature+ foreach (EditPoint e in ep) {+ if (e.get_active_handle () || e.selected_handle > 0)+ draw_edit_point_handles (e, cr);+ }+ + // control points+ foreach (EditPoint e in ep) {+ draw_edit_point (e, cr);+ }+ + }+ + private void draw_next (EditPoint e, EditPoint en, Context cr) {++ if (en.type == PointType.CURVE || e.type == PointType.CURVE) {+ draw_curve (e, en, cr);+ } else if (en.type == PointType.LINE && e.type == PointType.LINE) {+ draw_line (e, en, cr);+ } else if (unlikely (en.type != PointType.CURVE)) {+ warning (@"PointType is $(en.type)");+ }+ + }+ + private static void draw_curve (EditPoint e, EditPoint en, Context cr, double alpha = 1) {+ Glyph g = MainWindow.get_current_glyph ();+ double xa, ya, xb, yb, xc, yc, xd, yd;+ + get_bezier_points (e, en, out xa, out ya, out xb, out yb, out xc, out yc, out xd, out yd);++ cr.set_source_rgba (0, 0, 0, alpha);+ cr.set_line_width (1 * (1/g.view_zoom));+ + cr.line_to (xa, ya); // this point makes sense only if it is the first or last position, the other points are meaning less don't export them++ cr.curve_to (xb, yb, xc, yc, xd, yd);+ }+ + private static void print_curve (EditPoint e, EditPoint en) {+ double xa, ya, xb, yb, xc, yc, xd, yd;+ + get_bezier_points (e, en, out xa, out ya, out xb, out yb, out xc, out yc, out xd, out yd);++ stdout.printf ("L");+ + stdout.printf ("%s ", round (xa));+ stdout.printf ("%s ", round (ya)); + + stdout.printf ("\n");++ stdout.printf ("C");++ stdout.printf ("%s ", round (xb));+ stdout.printf ("%s ", round (yb));+ + stdout.printf ("%s ", round (xc));+ stdout.printf ("%s ", round (yc)); + + stdout.printf ("%s ", round (xd));+ stdout.printf ("%s ", round (yd)); + + stdout.printf ("\n");+ }+ + /** Curve relative to window center. */+ public static void get_bezier_points (EditPoint e, EditPoint en, out double xa, out double ya, out double xb, out double yb, out double xc, out double yc, out double xd, out double yd) {+ Glyph g = MainWindow.get_current_glyph ();+ + double center_x, center_y;+ + center_x = g.allocation.width / 2.0;+ center_y = g.allocation.height / 2.0;+ + xa = center_x + en.x;+ ya = center_y - en.y;++ xb = center_x + en.get_right_handle ().x ();+ yb = center_y - en.get_right_handle ().y ();+ + xc = center_x + e.get_left_handle ().x ();+ yc = center_y - e.get_left_handle ().y ();+ + xd = center_x + e.x;+ yd = center_y - e.y; + }++ /** Curve absolute glyph data. */+ public static void get_abs_bezier_points (EditPoint e, EditPoint en, out double xa, out double ya, out double xb, out double yb, out double xc, out double yc, out double xd, out double yd) {+ xa = + en.x;+ ya = - en.y;++ xb = + en.get_right_handle ().x ();+ yb = - en.get_right_handle ().y ();+ + xc = + e.get_left_handle ().x ();+ yc = - e.get_left_handle ().y ();+ + xd = + e.x;+ yd = - e.y; + }+ + /** Line points relative to center. */+ public static void get_line_points (EditPoint e, EditPoint en, out double xa, out double ya, out double xb, out double yb) {+ Glyph g = MainWindow.get_current_glyph ();+ + double xc = g.allocation.width / 2.0;+ double yc = g.allocation.height / 2.0;+ + xa = xc + en.x;+ ya = yc - en.y;+ + xb = xc + e.x;+ yb = yc - e.y;+ }++ public static void get_abs_line_points (EditPoint e, EditPoint en, out double xa, out double ya, out double xb, out double yb) {+ Glyph g = MainWindow.get_current_glyph ();+ + xa = en.x;+ ya = en.y;+ + xb = e.x;+ yb = e.y;+ }+ + public void draw_line (EditPoint e, EditPoint en, Context cr, double alpha = 1) { + Glyph g = MainWindow.get_current_glyph ();+ + double xc = g.allocation.width / 2.0;+ double yc = g.allocation.height / 2.0;+ + double ax, ay, bx, by;++ get_line_points (e, en, out ax, out ay, out bx, out by);+ + cr.set_source_rgba (0.4, 0.4, 0.4, alpha);+ cr.set_line_width (1 * (1/g.view_zoom));++ cr.line_to (ax, ay);+ + cr.line_to (bx, by);++ }+ + public void draw_edit_point (EditPoint e, Context cr) {+ draw_edit_point_center (e, cr);+ }+ + public void draw_edit_point_handles (EditPoint e, Context cr) {+ EditPoint handle_right = e.get_right_handle ().get_point ();+ EditPoint handle_left = e.get_left_handle ().get_point ();+ + draw_line (handle_right, e, cr, 0.15);+ draw_line (handle_left, e, cr, 0.15);+ + cr.stroke ();+ + draw_edit_point_center (handle_right, cr);+ draw_edit_point_center (handle_left, cr);+ }+ + + public static void draw_edit_point_center (EditPoint e, Context cr) {+ Glyph g = MainWindow.get_current_glyph ();+ + double ivz = 1 / g.view_zoom;++ double xc = g.allocation.width / 2.0;+ double yc = g.allocation.height / 2.0;++ double p = e.x;+ + double h = g.allocation.height * ivz + g.view_offset_y;+ + double x = xc + e.x;+ double y = yc - e.y + 0.5;++ if (e.active) + cr.set_source_rgba (1, 0, 0, 1);+ else+ cr.set_source_rgba (e.r, e.g, e.b, e.a);++ cr.set_line_width (3 * ivz); + + cr.new_path ();+ + cr.move_to (x - 0.7 * ivz, y - 0.7 * ivz - 0.5);+ cr.line_to (x + 0.7 * ivz, y + 0.7 * ivz - 0.5);+ + cr.move_to (x + 0.7 * ivz, y - 0.7 * ivz - 0.5);+ cr.line_to (x - 0.7 * ivz, y + 0.7 * ivz - 0.5);+ + cr.close_path ();++ //cr.fill_preserve ();+ cr.stroke ();++ }+ + public bool merge (Path? u) {+ bool err;+ int intersections; ++ Path union;+ Path merged;+ + if (u == null) return false;+ + union = (!) u;+ + // skip empty path and objects that does't overlap+ if (!is_valid_union (union)) {+ return false;+ }++ // add intersecting points+ intersections = prepare_merge (union);+ + if (intersections == 0) {+ return false;+ }++ // all outline paths are drawn clockwise+ err = force_union_directions (union);+ + if (err) {+ return false;+ }+ + // FIXME DELETE+ //print (@"DIR $(union.is_clockwise ())\n");+ //union.force_direction (Direction.COUNTER_CLOCKWISE);+ //print (@"DIR2 $(union.is_clockwise ())\n");+ + // create new paths+ merged = create_merged_path (this, union);++ // path is crossing it self or a new inside appeared+ /*+ if (merged.is_crossing_own_path ()) {+ if (this.is_crossing_own_path ()) {+ warning ("this.is_crossing_own_path ()");+ // TODO: split it and find some good way to deal with it+ } else {+ warning ("merged.is_crossing_own_path ()");+ return false;+ }+ }+ */++ // add merged points to this path+ this.remove_all_edit_points ();+ union.remove_all_edit_points ();+ replace_path (merged);+ + this.set_editable (true);+ union.set_editable (true);+ merged.set_editable (true);+ + return true;+ }+ + /** Returns true if there is an edit point at (p.x, p.y). */+ private bool has_edit_point (EditPoint p) {+ foreach (var t in points) {+ if (p.x == t.x && p.y == t.y) {+ return true;+ }+ }+ return false;+ }+ + private static unowned List<EditPoint>? first_outside (Path source, Path union) {+ unowned List<EditPoint> p = source.points.first ();+ bool begins_outside = false;+ for (int i = 0; i < p.length (); i++) {+ if (!union.is_over (p.data.x, p.data.y) && !union.has_edit_point (p.data)) {+ return p; // a good place to start+ }+ + p = p.next;+ }+ + return null;+ }+ + private bool force_union_directions (Path union) {+ bool r;+ + r = force_union_direction (union); // do force direction+ r = force_union_direction (union); // check result, direction should not change+ + if (unlikely (!r)) {+ warning ("Path direction is still not correct.");+ }+ + return true;+ }+ + private bool force_union_direction (Path union) {+ EditPoint? e;+ unowned List<EditPoint>? ed;+ EditPoint ep;+ EditPoint next;+ + create_list ();+ union.create_list ();+ + foreach (var p in points) {+ e = union.get_edit_point_at (p.x, p.y);+ + if (e != null) {+ ep = (!) e;+ ed = ep.get_next ();+ + if (unlikely (ed != null)) {+ warning ("next edit point is null");+ return false;+ }+ + next = ((!)ed).data;+ + if (is_over (ep.x, ep.y)) {+ union.reverse ();+ return false;+ }+ }+ }+ + return true;+ }+ + private static Path create_merged_path (Path source, Path union) {+ Path merged = new Path ();+ + uint ml = union.points.length () + source.points.length ();+ + unowned List<EditPoint> p;+ unowned List<EditPoint> eli;+ + unowned List<EditPoint>? fou;+ Path? swap;+ + // begin outside.+ fou = first_outside (source, union);+ if (fou == null) {+ swap = union;+ union = source;+ source = (!) swap;+ }+ + fou = first_outside (source, union);+ return_if_fail (fou != null);+ + p = (!) fou;+ eli = p;+ + // create a new path from this path + union+ bool u = false;+ uint i = 0;+ + while (true) {+ if (i == ml) break;+ if (++i == ml) { // FIXME: do something better+ break;+ }+ + if (!merged.has_edit_point_at (p.data.x, p.data.y)) {+ merged.add (p.data.x, p.data.y);+ }+ + eli = (!u) ? union.points : source.points;+ + int tni = 0; + foreach (var t in eli) {+ + if (p.data.x == t.x && p.data.y == t.y) {+ p = eli.nth (tni);+ u = !u; + break;+ }+ + tni++;+ }+ + if (p == p.last ()) p = p.first ();+ else p = p.next;+ }+ + return merged; + }+ + private bool has_edit_point_at (double x, double y) {+ foreach (var p in points) {+ if (p.x == x && p.y == y) return true;+ }+ + return false;+ }++ private EditPoint? get_edit_point_at (double x, double y) {+ foreach (var p in points) {+ if (p.x == x && p.y == y) return p;+ }+ + return null;+ }+ + private void remove_all_edit_points () {+ while (points.length () > 0) {+ points.remove_link (points.first ());+ }+ }+ + /** Replace edit points in this path with @param new_path.points. */+ public void replace_path (Path new_path) {+ while (points.length () > 0) {+ points.remove_link (points.first ());+ }+ + foreach (var np in new_path.points) {+ add (np.x, np.y);+ }+ + close ();+ }+ + /** Returns true if this path can be merged with @param union */ + private bool is_valid_union (Path union) {+ if (union == this) {+ warning ("Path can't union with it self.");+ return false;+ }+ + if (union.points.length () <= 2 || points.length () <= 2) {+ return false;+ }++ if (!has_overlapping_boundry (union)) {+ return false;+ }++ return true;+ }+ + /** Set direction for this path to clockwise for outline and + * counter clockwise for inline paths.+ */+ public bool force_direction (Direction direction) {+ bool c = (direction == Direction.CLOCKWISE);+ + if (c != is_clockwise ()) {+ this.reverse ();+ }+ + if (unlikely (is_clockwise () != c)) {+ warning ("Failed to set direction for path in force_direction.");+ return true;+ }+ + return false;+ }+ + /** Add start and stop points to union from intersecting lines. + * @return number of new intersecting points+ */+ private int prepare_merge (Path union) + requires (union.points.length () >= 2)+ { + unowned List<EditPoint> lep = union.points.first ();+ unowned List<EditPoint> lprev = union.points.last ();++ List<IntersectionList> inter = new List<IntersectionList> ();+ int count = 0;+ + while (true) {+ IntersectionList il = get_all_crossing_paths (lprev.data, lep.data);+ inter.append (il);++ if (lep == union.points.last ()) break;+ + lprev = lep;+ lep = lep.next;+ }++ foreach (var ins in inter) {+ foreach (var ip in ins.points) {+ add_point_on_path (ip.x, ip.y);+ union.add_point_on_path (ip.x, ip.y);+ count++;+ }+ }+ + return count;+ }++ /** Returns true if this path crosses it self some where. */+ public bool is_crossing_own_path () {+ unowned List<EditPoint> lep;+ unowned List<EditPoint> lprev;+ + if (points.length () <= 2) return false;+ + lep = points.first ();+ lprev = points.last ();++ while (true) {+ if (get_all_crossing_paths (lprev.data, lep.data).points.length () > 0) {+ return true;+ }++ if (lep == points.last ()) break;+ + lprev = lep;+ lep = lep.next;+ }++ return false;+ }++ public void delete_edit_point (EditPoint e) {+ foreach (var p in points) {+ if (likely (p == e)) {+ points.remove (p);+ }+ }+ + warning ("Edit point does not exist.");+ }+ +// TODO: Move classes+class Intersection : GLib.Object {+ public double x;+ public double y;+ + public Intersection (double x, double y) {+ this.x = x;+ this.y = y;+ }+ +}++class IntersectionList : GLib.Object {+ public List<Intersection> points = new List<Intersection> ();+ + public IntersectionList () { + }+ + public void add (double x, double y) {+ if (points.length () > 0 && points.last().data.x == x && points.last().data.y == y) {+ return;+ }+ + points.append (new Intersection (x, y));+ }+ + public void delete_control_point_at (double x, double y) {+ foreach (var p in points) {+ if (p.x == x && p.y == y) {+ points.remove (p);+ }+ }+ } +}++ private static bool line_is_duplicate (EditPoint a_start, EditPoint a_stop, EditPoint b_start, EditPoint b_stop) {+ return line_is_duplicate_comparaton (a_start, a_stop, b_start, b_stop)+ || line_is_duplicate_comparaton (a_stop, a_start, b_start, b_stop); // swap and compare+ }++ private static bool line_is_duplicate_comparaton (EditPoint a_start, EditPoint a_stop, EditPoint b_start, EditPoint b_stop) {+ return (a_start.x == b_start.x && a_start.y == b_start.y && a_stop.x == b_stop.x && a_stop.y == b_stop.y);+ }+ + /** Returns a list of all points where curves in this path crosses + * line from @param start to @param stop+ */+ private IntersectionList get_all_crossing_paths (EditPoint start, EditPoint stop) {+ IntersectionList il = new IntersectionList ();+ + unowned List<EditPoint> prev = points.last ();+ unowned List<EditPoint> ep = points.first ();+ + double new_x;+ double new_y;++ return_val_if_fail (points.length () >= 2, new IntersectionList ());++ while (true) {++ bool r = false; // fixme: optimize (don't iterate + stop iteration)+ double min = double.MAX;+ + new_x = double.MAX;+ new_y = double.MAX;+ + all_of (prev.data, ep.data, (tx, ty) => {+ all_of (start, stop, (cx, cy) => {+ double n = pow (tx - cx, 2) + pow (ty - cy, 2);+ + if (n < min && n < 1) { // TODO: add variable tolerance+ r = true;+ min = n;++ // this point is a better coordinate, replace the last one+ il.delete_control_point_at (new_x, new_y);+ + new_x = cx;+ new_y = cy;++ //il.add_unique (new_x, new_y); // add a new point+ il.add (new_x, new_y); // add a new point+ }+ + // Look for the next one a long this path+ if (n > 1) {+ min = double.MAX;+ }+ + return true;+ }, 5);+ + return true;+ }, 5);+ + + if (ep == points.last ()) break;+ + prev = ep;+ ep = ep.next;+ }++ return il;+ }++ /** Switch direction from clockwise path to counter clockwise path or vise versa. */+ public void reverse () {+ bool direction = is_clockwise ();+ + if (no_derived_direction) {+ clockwise_direction = !clockwise_direction;+ }+ + reverse_points ();+ + if (unlikely (direction == is_clockwise ())) {+ stderr.printf ("Error: Direction did not change after reversing path.\n");+ stderr.printf (@"Length: $(points.length ())\n");+ stderr.printf (@"No particular direction can be derived: $no_derived_direction \n");+ warning ("Path.reverse () failed.\n");+ }+ }++ private void reverse_points () {+ points.reverse ();+ + EditPointHandle t;+ foreach (var e in points) {+ t = e.right_handle;+ e.right_handle = e.left_handle;+ e.left_handle = t;+ }+ + create_list ();+ }++ private int index (EditPoint ep) {+ int i = 0;+ + foreach (var p in points) {+ if (ep.x == p.x && ep.y == p.y) {+ return i;+ }+ + i++;+ }+ + return -1;+ }++ public void print_all_points () {+ int i = 0;+ foreach (var p in points) {+ ++i;+ string t = (p.type == PointType.END) ? " endpoint" : "";+ stdout.printf (@"Point $i at ($(p.x), $(p.y)) $t \n");+ }+ }++ // Fixa+ private int next_point_is_clockwise (EditPoint p) {+ if (p.direction == PointDirection.DOWN_RIGHT) {+ if (p.get_next ().data.direction == PointDirection.DOWN_LEFT)+ return -1;+ else+ return +1;+ }+ + if (p.direction == PointDirection.UP_RIGHT) {+ if (p.get_next ().data.direction == PointDirection.DOWN_RIGHT)+ return -2;+ else+ return +2;+ }+ + if (p.direction == PointDirection.UP_LEFT) {+ if (p.get_next ().data.direction == PointDirection.UP_RIGHT)+ return -3;+ else+ return +3;+ }+ + if (p.direction == PointDirection.DOWN_LEFT) {+ if (p.get_next ().data.direction == PointDirection.UP_LEFT)+ return -4;+ else+ return +4;+ }++ warn_if_reached ();+ return 0;+ }+ + public bool is_clockwise () {+ int i, r;+ + if (unlikely (points.length () <= 2)) {+ no_derived_direction = true;+ return clockwise_direction;+ }++ reverse_points ();+ create_list ();+ set_directions (); + r = points_direction ();++ reverse_points ();+ create_list ();+ set_directions ();+ i = points_direction ();+ + /*+ if (i < r) print (@"Ja den är medsols \n");+ else print (@"Nä den är inte medsols\n");+ print (@" i > r; $i > $r \n\n");+ */+ + if (unlikely (i == r)) {+ warning (@"Can not determine directions since i == r ($i == $r)\n");+ }+ + return i < r;+ }+ + private int points_direction () {+ int dir = 0;+ foreach (var p in points) {+ dir += next_point_is_clockwise (p);+ }+ return dir; + }+ + public static bool next_triangle_is_clockwise (EditPoint a) {+ unowned List<EditPoint>? p = a.get_next ();+ unowned List<EditPoint>? q = ((!)p).next;+ + return_if_fail (p != null);+ + if (q == null) q = ((!)p).first ();+ + return_if_fail (q != null);+ + return triangle_is_clockwise (a, ((!)p).data, ((!)q).data);+ }+ + public static bool triangle_is_clockwise (EditPoint a, EditPoint b, EditPoint c) {+ + if (a.x <= b.x <= c.x) return (a.y < c.y);+ if (a.x < b.x <= c.x) return (a.y < c.y);+ if (a.x >= b.x && c.x >= b.x) return (a.y > c.y);+ if (a.x < b.x && c.x < b.x) return (a.y > c.y); + if (a.x >= b.x && c.x <= b.x) return (c.y > a.y);+ if (a.x <= b.x && c.x <= b.x) return (c.y < a.y);+ if (a.x <= b.x && c.x >= b.x) return (c.y > a.y);+ + warning ("Reached.");+ return false;+ }++ private bool is_clockwise_extrema (int ul, int ur, int lr, int ll) {+ int t;+ int i;++ for (i = 0; i < 4; i++) {++ if (ul <= ur <= lr <= ll) {+ return false;+ }+ + if (ul >= ur >= lr <= ll) {+ return true;+ }+ + // shift+ t = lr;+ lr = ll;+ ll = ul;+ ul = ur;+ ur = t;+ }++ warning (@"($ul <= $ur <= $lr <= $ll) \n");+ warn_if_reached ();+ + return false;+ }++ private static uint num_positions (uint i_xmax, uint i_ymax, uint i_xmin, uint i_ymin) {+ List<uint> i = new List<uint> ();+ + if (i.index (i_xmax) == -1) i.append (i_xmax);+ if (i.index (i_ymax) == -1) i.append (i_ymax);+ if (i.index (i_xmin) == -1) i.append (i_xmin);+ if (i.index (i_ymin) == -1) i.append (i_ymin);++ return i.length ();+ }+ + public bool is_editable () {+ return edit;+ }++ /** Show control points on outline path. */+ public void set_editable (bool e) {+ edit = e;+ }+ + public bool is_open () {+ return open;+ }+ + private bool direction_x (EditPoint start, EditPoint stop) {+ return (start.x < stop.x);+ }++ private bool direction_y (EditPoint start, EditPoint stop) {+ return (start.y < stop.y);+ }+ + public void thinner (double ratio) + requires (points.length () > 2)+ { + update_region_boundries ();+ + double w = Math.fabs(xmax - xmin);+ double h = Math.fabs(ymax - ymin);+ double cx = w / 2 + xmin; + double cy = h / 2 + ymin;+ + ratio = 1 - ratio;++ unowned List<EditPoint> i = points.first ();+ unowned List<EditPoint> prev = points.last ();+ + bool dirx = true;+ bool ldirx = direction_x (prev.data, i.data);+ bool ndirx;++ bool diry = true;+ bool ldiry = direction_y (prev.data, i.data);+ bool ndiry;+ + while (true) {+ ndirx = direction_x (prev.data, i.data);+ + if (ndirx != ldirx) {+ dirx = !dirx;+ }+ + ldirx = ndirx;++ ndiry = direction_y (prev.data, i.data);+ + if (ndiry != ldiry) {+ diry = !diry;+ }+ + ldiry = ndiry;+ + EditPoint p = i.data;+ + if (dirx) {+ if (p.x < cx)+ p.x += (cx - p.x) * ratio;+ else+ p.x -= (p.x - cx) * ratio;+ } else {+ if (p.x < cx)+ p.x += (cx - p.x) * ratio * 2;+ else+ p.x -= (p.x - cx) * ratio * 2;+ }++ // err borde vara -y+ if (p.y < cy && diry || p.y > cy && ! diry)+ p.y += (cy - p.y) * ratio;+ else+ p.y -= (p.y - cy) * ratio; + + if (i == points.last ()) {+ break;+ }+ + prev = i;+ i = i.next;+ }+ }+ + public void resize (double ratio) {+ double w, h, cx, cy;+ + update_region_boundries ();+ + w = Math.fabs(xmax - xmin);+ h = Math.fabs(ymax - ymin);+ cx = w / 2 + xmin; + cy = h / 2 + ymin;+ + if (ratio == 1) return;+ + if (ratio < 1) {+ ratio = 1 - ratio;+ + foreach (var p in points) {+ if (p.x < cx)+ p.x += (cx - p.x) * ratio;+ else+ p.x -= (p.x - cx) * ratio;++ if (p.y > cy)+ p.y += (cy - p.y) * ratio;+ else+ p.y -= (p.y - cy) * ratio; + }+ + } else {+ ratio = ratio - 1;+ + foreach (var p in points) {+ if (p.x < cx)+ p.x -= (cx - p.x) * ratio;+ else+ p.x += (p.x - cx) * ratio;++ if (p.y > cy)+ p.y -= (cy - p.y) * ratio;+ else+ p.y += (p.y - cy) * ratio; + }+ + }+ }+ + public Path copy () {+ Path new_path = new Path ();+ + foreach (var ep in points) {+ new_path.add_point (ep.copy ());+ }+ + new_path.edit = edit;+ new_path.open = open;+ + new_path.create_list ();+ + return new_path;+ } + + public bool is_over (double x, double y) {+ if (!is_over_boundry (x, y)) {+ return false;+ }++ return true;+ }++ bool in_line_region (EditPoint ep, EditPoint e, double x, double y) {+ Glyph g = MainWindow.get_current_glyph ();+ + x = x * Glyph.ivz () + g.view_offset_x - Glyph.xc ();+ y = y * Glyph.ivz () + g.view_offset_y - Glyph.yc ();++ y *= -1;+ + return ((ep.x <= x <= e.x || e.x <= x <= ep.x) && (ep.y <= y <= e.y || e.y <= y <= ep.y));+ }++ public bool is_over_boundry (double x, double y) {+ Glyph g = MainWindow.get_current_glyph ();+ + x = x * Glyph.ivz () + g.view_offset_x - Glyph.xc ();+ y = y * Glyph.ivz () + g.view_offset_y - Glyph.yc ();++ y *= -1;+ + return (ymin <= y <= ymax) && (xmin <= x <= xmax);+ }++ public bool has_overlapping_boundry (Path p) {+ return !(xmax < p.xmin || ymax < p.ymin) || (xmin > p.xmax || ymin > p.ymax);+ }+ + public void add (double x, double y) {+ add_after (x, y, points.last ());+ }++ public void add_point (EditPoint p) {+ add_point_after (p, points.last ());+ }++ /** Insert a new point after @param previous_point and return a reference + * to the new item in list.+ */+ public unowned List<EditPoint> add_after (double x, double y, List<EditPoint>? previous_point) {+ EditPoint p = new EditPoint (x, y, PointType.LINE); + return add_point_after (p, previous_point);+ }+ + public unowned List<EditPoint> add_point_after (EditPoint p, List<EditPoint>? previous_point) {+ unowned List<EditPoint> np;+ unowned List<EditPoint>? op;+ int prev_index;++ if (points.length () > 0 && previous_point != null && ((!)previous_point).data.type == PointType.END) {+ points.delete_link ((!) previous_point);+ }++ if (unlikely (previous_point == null && points.length () != 0)) warning ("previous_point == null");++ if (points.length () == 0) {+ points.append (p);+ np = points.last ();+ p.prev = points.last ();+ p.next = points.last ();+ } else {+ p.prev = (!) previous_point;+ p.next = ((!) previous_point).next;++ points.insert_before (((!) previous_point).next, p);+ + prev_index = points.position ((!) previous_point);+ np = points.nth (prev_index + 1);+ }+ + second_last_point = last_point;+ last_point = p;+ + update_region_boundries ();+ + return np;+ + }+ + public void add_point_on_path (double x, double y) {+ EditPoint e = new EditPoint (0, 0);+ get_closest_point_on_path (e, x, y);+ add_after (x, y, e.prev);+ }+ + public void close () {+ open = false;+ edit = false;+ }+ + public void reopen () {+ open = true;+ edit = true;+ }+ + public EditPoint? get_last_point () {+ return last_point;+ }++ public EditPoint? get_second_last_point () {+ return second_last_point;+ }+ + /** Move edit point. */ + public void move_last_point (double x, double y) {+ return_if_fail (last_point != null);+ ((!) last_point).set_position (x, y);+ update_region_boundries ();+ }+ + /** Move path. */+ public void move (double delta_x, double delta_y) {+ foreach (var ep in points) {+ ep.x += delta_x;+ ep.y += delta_y; // kanske -y+ }+ + update_region_boundries ();+ }+ + /** Remove intersection. */+ public void cut_path (Path exclution) {+ if (!has_intersection (exclution)) {+ stderr.printf ("No intersec in cut path.\n");+ return;+ }+ + if (points.length () < 1 || exclution.points.length () < 1) {+ stderr.printf ("No intersection in cut path: %u %u\n", points.length (), exclution.points.length ());+ }+ + EditPoint prev_tp = points.last ().data;+ EditPoint prev_ep = exclution.points.last ().data;+ foreach (EditPoint ep in exclution.points) {+ + int insert_index = 0;+ foreach (EditPoint tp in points) {+ + if (point_in_range ((!) prev_tp, ep, tp)) {+ EditPoint? ip = get_intersect_point (prev_tp, tp, prev_ep, ep);+ if (ip == null) {+ stderr.printf ("No intersecting point to cut from.\n");+ continue;+ }+ + EditPoint inter_start = (!) ip;+ + points.insert (inter_start, insert_index);+ } else {+ stderr.printf ("No point in range\n");+ }+ + insert_index++;+ prev_tp = tp;+ }+ + prev_ep = ep;+ } + }++ /** Test if @param path is a valid outline for this object. */ + public bool test_is_outline (Path path) {+ return this.test_is_outline_of_path (path) && path.test_is_outline_of_path (this);+ }+ + private bool test_is_outline_of_path (Path outline)+ requires (outline.points.length () >= 2 || points.length () >= 2)+ { + // rather slow use it for testing, only+ unowned List<EditPoint> i = outline.points.first ();+ unowned List<EditPoint> prev = i.last ();++ double tolerance = 1;+ bool g = false;+ + EditPoint ep = new EditPoint (0, 0);+ double min = double.MAX;+ + while (true) {+ min = 10000;+ + all_of (prev.data, i.data, (cx, cy) => {+ get_closest_point_on_path (ep, cx, cy);++ double n = pow (ep.x - cx, 2) + pow (cy - ep.y, 2);+ + if (n < min) min = n;+ + if (n < tolerance) {+ g = true;+ return false;+ }++ return true;+ });+ + if (!g) {+ critical (@"this path does not seem to be the outline. (min $min)");+ }+ + g = false;+ + if (i == i.last ()) {+ break;+ }+ + i = i.next;+ }+ + return true;+ }+ + /** Get a point on the this path closest to x and y coordinates. */+ public void get_closest_point_on_path (EditPoint edit_point, double x, double y) {+ return_if_fail (points.length () != 0);+ + double min = double.MAX;+ double n = 0;+ bool g = false;+ + double ox = 0;+ double oy = 0;+ + unowned List<EditPoint> i = points.first ();+ unowned List<EditPoint> prev = i.next;++ bool done = false;+ bool exit = false;+ + unowned List<EditPoint>? previous_point = null;+ unowned List<EditPoint>? next_point = null;+ + while (!exit) {+ + if (i == points.last ()) {+ done = true;+ }+ + if (!done) {+ i = i.next;+ prev = i.prev;+ } else if (done && !is_open ()) {+ i = points.first ();+ prev = points.last ();+ exit = true;+ } else {+ break;+ }+ + all_of (prev.data, i.data, (cx, cy) => {+ n = pow (x - cx, 2) + pow (y - cy, 2);+ + if (n < min) {+ min = n;+ + ox = cx;+ oy = cy;+ + previous_point = prev;+ next_point = i;+ + g = true;+ }+ + return true;+ });+ }++ edit_point.set_position (ox, oy);+ edit_point.prev = previous_point;+ edit_point.next = next_point;++ if (unlikely (!g)) {+ warning (@"Error: Got no coordinates for point on path. Num points $(points.length ())\n");+ }+ }++ // TODO: Find a clever mathematical solutions instead+ private void all_of (EditPoint start, EditPoint stop, RasterIterator iter, int steps = 400) {+ int n = steps;+ + double px;+ double py;++ double rx, qx;+ double ry, qy;++ double sx = (stop.x - start.x) / n;+ double sy = (stop.y - start.y) / n;++ if (false && start.type == PointType.LINE && stop.type == PointType.LINE) {+ all_of_line (start.x, start.y, stop.x, stop.y, iter, steps);+ } else {+ all_of_curve (start.x, start.y, start.get_right_handle ().x (), start.get_right_handle ().y (), start.get_left_handle ().x (), start.get_left_handle ().y (), stop.x, stop.y, iter, steps);+ }++ }++ private void all_of_curve (double x0, double y0, double x1, double y1, double x2, double y2, double x3, double y3, RasterIterator iter, int steps = 400) {+ double px = x1;+ double py = y1;+ + double t;+ double s = steps;+ + for (int i = 0; i < steps; i++) {+ t = i / s;+ + px = pow (1 - t, 3) * x0;+ px += 3 * pow (1 - t, 2) * t * x1;+ px += 3 * pow (1 - t, 2) * pow (t, 2) * x2;+ px += pow (t, 3) * x3;++ py = pow (1 - t, 3) * y0;+ py += 3 * pow (1 - t, 2) * t * y1;+ py += 3 * pow (1 - t, 2) * pow (t, 2) * y2;+ py += pow (t, 3) * y3;+ + if (!iter (px, py)) {+ return;+ }+ + } + }++ private void all_of_line (double x1, double y1, double x2, double y2, RasterIterator iter, int steps = 400) {+ int n = steps;+ + double px = x1;+ double py = y1;++ double sx = (x2 - x1) / n;+ double sy = (y2 - y1) / n;++ for (int i = 0; i < n; i++) {+ + if (!iter (px, py)) {+ return;+ }+ + px += sx;+ py += sy;+ }++ }+ + private EditPoint? get_intersect_point (EditPoint a_start, EditPoint a_stop, EditPoint b_start, EditPoint b_stop) {+ double ax = a_start.x;+ double ny = a_start.y;+ + double bx = b_start.x;+ + int n = 100;+ + double a_step_x = (a_stop.x - a_start.x) / n;+ double b_step_x = (b_stop.x - b_start.x) / n;++ bool d = !(ax > bx);++ for (int i = 0; i < n; i++) {+ + // if (nx > x.x) { // Nästan grekiskt, kolla parabol + // eller bx > ax åt andra hållet+ if (d && ax > bx) { // överklart klart, asymptotiskt + return new EditPoint (ax, a_start.y * n/100.0); // fixme do y to+ }++ if (!d && ax < bx) {+ return new EditPoint (ax, a_start.y * n/100.0);+ }+ + ax += a_step_x;+ bx += b_step_x;+ }++ return null;+ }+ + private bool point_in_range (EditPoint x, EditPoint a, EditPoint b) {+ return in_range (x.x, a.x, b.x) && in_range (x.y, a.y, b.y);+ }+ + private bool in_range (double x, double ma, double mb) {+ return (ma < x < mb) || (ma > x > mb);+ }+ + private bool has_intersection (Path p) {+ return p.has_intersecting_point (this) || this.has_intersecting_point (p);+ }++ private bool has_intersecting_point (Path p) {+ return (is_over (p.xmin, p.ymin) || is_over (p.xmax, p.ymax));+ }+ + public void update_region_boundries () {+ unowned List<EditPoint> i = points.first ();+ unowned List<EditPoint> prev = points.last ();++ if (points.length () == 0) {+ xmax = 0;+ xmin = 0;+ ymax = 0;+ ymin = 0; + }++ xmax = -10000;+ xmin = 10000;+ ymax = -10000;+ ymin = 10000;++ double txmax = -10000;+ double txmin = 10000;+ double tymax = -10000;+ double tymin = 10000;++ bool new_val = false;+ + while (i != points.last ()) {+ + all_of (prev.data, i.data, (cx, cy) => {+ + if (!new_val) {+ txmax = cx;+ txmin = cx;+ tymax = cy;+ tymin = cy;+ new_val = true;+ }+ + if (cx < txmin) {+ txmin = cx;+ }++ if (cx > txmax) {+ txmax = cx;+ }+ + if (cy < tymin) {+ tymin = cy;+ }++ if (cy > tymax) {+ tymax = cy;+ }++ return true;+ });++ prev = i;+ i = i.next;+ }++ xmax = txmax;+ xmin = txmin;+ ymax = tymax;+ ymin = tymin;++ // Fixa:+ // print (@"updated \n");++ if (unlikely (!new_val)) {+ // only one point, what should we do? probably skip it.+ } else if (unlikely (!got_region_boundries ())) {+ warning (@"No new region boundries.\nPoints.length: $(points.length ())");+ print_boundries ();+ }+ }+ + public void print_boundries () {+ stderr.printf (@"xmax $xmax \n");+ stderr.printf (@"xmin $xmin \n");+ stderr.printf (@"ymax $ymax \n");+ stderr.printf (@"ymin $ymin \n"); + }+ + public bool got_region_boundries () {+ return !(xmax == -10000 || xmin == 10000 || ymax == -10000 || ymin == 10000);+ }+ + public void create_list () {+ unowned List<EditPoint> ep = points;+ + if (points.length () == 0) {+ return;+ }+ + if (points.length () == 1) {+ ep.data.next = points.first ();+ ep.data.prev = points.last ();+ return;+ }+ + ep.data.next = ep.next;+ ep.data.prev = ep.last ();+ + while (ep != ep.last ()) {+ ep.data.next = ep.next;+ ep.data.prev = ep.prev;+ ep = ep.next;+ }+ + ep.data.next = ep.first ();+ ep.data.prev = ep.prev;+ }+ + public void set_directions () {+ create_list ();+ + if (points.length () <= 1) return;+ + foreach (var ep in points) {+ ep.set_direction ();+ }+ }+ + public void set_new_start (EditPoint ep) {+ List<EditPoint> list = new List<EditPoint> ();+ uint len = points.length ();+ unowned List<EditPoint> iter = points.first ();+ + foreach (var it in points) {+ if (it == ep) {+ break;+ }+ + iter = iter.next;+ }+ + for (uint i = 0; i < len; i++) {+ list.append (iter.data);+ + if (iter == iter.last ())+ iter = iter.first ();+ else+ iter = iter.next;+ + }+ + points = (owned) list;+ }+ +}++}new file mode 100644@@ -0,0 +1,617 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class PenTool : Tool {++ public static bool move_selected = false;+ public static bool begin_new_point_on_path = false;+ public static bool move_point_on_path = false;++ public static bool edit_active_corner = false;+ public static EditPoint active_corner = new EditPoint ();+ public static EditPoint selected_corner = new EditPoint ();++ public static EditPointHandle active_handle = new EditPointHandle.empty ();+ public static EditPointHandle selected_handle = new EditPointHandle.empty ();+ + public static EditPoint? active_edit_point = new EditPoint ();+ public static EditPoint selected_edit_point = new EditPoint ();+ + /** Move handle vertical or horizontal. */+ public static bool tie_x_or_y_coordinates = false;++ private static bool move_selected_handle = false;++ public PenTool (string name) {+ base (name, "Edit path", 'p', CTRL);+ + select_action.connect((self) => {+ });++ deselect_action.connect ((self) => {+ Glyph glyph = MainWindow.get_current_glyph ();+ + CutTool.force_direction ();+ glyph.close_path ();+ + move_point_on_path = false; // Fixa: this might be better as toggle buttons+ begin_new_point_on_path = false;+ });+ + press_action.connect((self, b, x, y) => {+ press (b, x, y);+ });++ release_action.connect((self, b, ix, iy) => {+ double x = ix;+ double y = iy;+ Glyph g = MainWindow.get_current_glyph ();+ + if (move_selected && GridTool.is_visible ()) {+ GridTool.tie (ref x, ref y);+ g.move_selected_edit_point (x, y);+ }++ move_selected = false;+ move_selected_handle = false;+ edit_active_corner = false;+ + selected_handle = new EditPointHandle.empty ();+ active_handle = new EditPointHandle.empty (); + });++ move_action.connect((self, x, y) => {+ move (x, y);+ });+ }+ + public void move (double x, double y) {+ Glyph glyph = MainWindow.get_current_glyph ();+ + control_point_event (x, y);+ + if (move_selected) {+ glyph.move_selected_edit_point (x, y);+ + if (tie_x_or_y_coordinates) {+ return_if_fail (glyph.selected_point != null);+ + EditPoint ep = (!) glyph.selected_point;+ GridTool.tie_to_prev (ep, x, y);+ }+ + }+ + if (begin_new_point_on_path) {+ move_current_point_on_path (x, y);+ }+ + if (is_corner_selected ()) {+ if (active_corner != selected_corner) {+ active_corner.set_active_handle (false);+ }+ + if (!move_selected_handle && !is_over_handle (x, y) && active_edit_point != null) { + active_corner = (!) active_edit_point;+ active_corner.set_active_handle (true);+ }+ + if (move_selected_handle) {+ if (GridTool.is_visible ()) {+ GridTool.tie (ref x, ref y);+ }+ + selected_handle.parent.set_point_type (PointType.CURVE);+ selected_handle.move_to (x, y);+ + selected_handle.parent.get_prev ().data.set_point_type (PointType.CURVE);+ + // Fixa: redraw line only+ glyph.redraw_area (0, 0, glyph.allocation.width, glyph.allocation.height); + }+ }+ + }++ public void move_current_point_on_path (double x, double y) {+ Glyph g = MainWindow.get_current_glyph ();++ if (g.new_point_on_path == null) {+ return;+ }+ + if (GridTool.is_visible ()) {+ GridTool.tie (ref x, ref y);+ }+ + return_if_fail (g.active_path != null);+ return_if_fail (g.new_point_on_path != null);+ + EditPoint ep = (!) g.new_point_on_path;+ Path p = (!) g.active_path;+ + double rax, ray;+ double pax, pay;++ pax = x * Glyph.ivz () + g.view_offset_x - Glyph.xc ();+ pay = y * Glyph.ivz () + g.view_offset_y - Glyph.yc ();++ pay *= -1;++ p.get_closest_point_on_path (ep, pax, pay);+ + rax = (pax + g.view_offset_x - Glyph.xc ()) / Glyph.ivz ();+ ray = (pax + g.view_offset_x - Glyph.xc ()) / Glyph.ivz ();+ + g.redraw_area (rax - 5, ray - 5, 10, 10);+ }++ public void set_new_point_on_path (Path ap, int x, int y) {+ Glyph g = MainWindow.get_current_glyph ();+ + return_if_fail (ap.is_editable ());+ + g.active_path = ap;++ if (g.new_point_on_path == null) {+ g.new_point_on_path = new EditPoint (0, 0, PointType.FLOATING);+ }+ + move_current_point_on_path (x, y);+ }+ + public static void set_active_edit_point (EditPoint? e) {+ Glyph g = MainWindow.get_current_glyph ();+ foreach (var p in g.path_list) {+ foreach (var ep in p.points) {+ ep.set_active (false);+ }+ }+ + active_edit_point = e;+ + if (e != null) {+ ((!)e).set_active (true);+ }++ g.queue_draw_area (0, 0, g.allocation.width, g.allocation.height);+ }++ public void control_point_event (double event_x, double event_y) {+ Glyph g = MainWindow.get_current_glyph ();+ + double x = event_x * Glyph.ivz () + g.view_offset_x - Glyph.xc ();+ double y = event_y * Glyph.ivz () + g.view_offset_y - Glyph.yc ();++ double m = double.MAX;+ double d = 0;+ + y *= -1;++ set_active_edit_point (null);+ + foreach (Path current_path in g.path_list) {+ if (!current_path.is_editable ()) {+ continue;+ }++ foreach (EditPoint e in current_path.points) {+ d = Math.sqrt (Math.fabs (Math.pow (e.x - x, 2) + Math.pow (e.y - y, 2)));+ + if (d < m && d * g.view_zoom < 14) {+ m = d;+ set_active_edit_point (e);+ }+ }++ }+ + }+ + public static bool is_move_point_selected () {+ Tool t = MainWindow.get_toolbox ().get_tool ("move_point");+ return t.is_selected ();+ }++ public static bool is_new_point_on_path_selected () {+ Tool t = MainWindow.get_toolbox ().get_tool ("new_point_on_path");+ return t.is_selected ();+ }++ public static bool is_new_point () {+ Tool t = MainWindow.get_toolbox ().get_tool ("new_point");+ return t.is_selected ();+ }++ public static bool is_corner_selected () {+ Tool t = MainWindow.get_toolbox ().get_tool ("corner");+ return t.is_selected ();+ }+ + public static bool is_erase_selected () {+ Tool t = MainWindow.get_toolbox ().get_tool ("erase_tool");+ return t.is_selected ();+ }+ + public void new_point_action (int x, int y) {+ Glyph glyph;+ glyph = MainWindow.get_current_glyph ();+ glyph.open_path ();+ glyph.add_new_edit_point (x, y);+ move_selected = true;+ }++ public void press (int button, int x, int y) {+ Path cp;+ Glyph? g = MainWindow.get_current_glyph ();+ Glyph glyph = (!) g;+ + return_if_fail (g != null);+ + glyph.open_path ();+ + control_point_event (x, y);+ + // select or deselect a path+ if (button == 3) {+ if (glyph.has_active_path ()) {+ CutTool.force_direction ();+ glyph.close_path ();+ } else {+ glyph.open_path ();+ open_closest_path (x, y);+ }+ }+ + if (!new_point_on_path_at (x, y)) {+ return;+ }+ + if (!is_move_point_selected ()) { + move_selected = false;+ move_point_on_path = false;+ }+ + if (is_move_point_selected () && active_edit_point != null) {+ glyph.store_undo_state ();+ + if (glyph.has_active_path ()) {+ cp = (!) glyph.active_path;+ foreach (var e in cp.points) {+ e.set_active (false);+ }+ }+ + move_selected = true;+ move_point_on_path = true;+ + glyph.selected_point = active_edit_point;+ } + + if (is_new_point () && button == 1) {+ glyph.store_undo_state ();+ new_point_action (x, y);+ }+ + if (is_corner_selected ()) {+ if (!is_over_handle (x, y)) {+ selected_corner.set_active_handle (false);+ edit_active_corner = true;+ set_default_handle_positions ();+ + if (active_edit_point != null) {+ selected_corner = (!) active_edit_point;+ }+ }+ + curve_corner_event (x, y);+ }+ + if (is_erase_selected () && active_edit_point != null) {+ glyph.store_undo_state ();+ glyph.delete_edit_point ((!) active_edit_point);+ }+ + }++ void set_default_handle_positions () {+ Glyph g = MainWindow.get_current_glyph ();+ foreach (var p in g.path_list) {+ if (p.is_editable ()) {+ p.create_list ();+ set_default_handle_positions_on_path (p);+ }+ }+ }++ void set_default_handle_positions_on_path (Path path) {+ EditPointHandle h;+ EditPoint n;+ double nx, ny;+ + // set handle pointing at next point to create a line to next point+ // as default handle position for LINE points.+ + foreach (EditPoint e in path.points) {+ if (e.type == PointType.LINE) {+ n = e.get_next ().data;+ h = e.get_right_handle ();+ + nx = e.x + ((n.x - e.x) / 3);+ ny = e.y + ((n.y - e.y) / 3);+ + h.move_to_coordinate (nx, ny);+ }+ }+ + foreach (EditPoint e in path.points) {+ if (e.type == PointType.LINE) {+ n = e.get_prev ().data;+ h = e.get_left_handle ();+ + nx = e.x + ((n.x - e.x) / 3);+ ny = e.y + ((n.y - e.y) / 3);+ + h.move_to_coordinate (nx, ny);+ }+ }+ }++ void open_closest_path (double x, double y) {+ Path? p;+ Glyph g = MainWindow.get_current_glyph ();++ if (g.path_list.length () == 0) return;+ + p = g.get_closeset_path (x, y);+ + if (p != null) {+ ((!)p).set_editable (true);+ g.open_path ();+ }+ }++ bool new_point_on_path_at (int x, int y) {+ EditPoint ep;+ Glyph glyph = MainWindow.get_current_glyph ();+ + if (is_new_point_on_path_selected ()) {+ + if (glyph.new_point_on_path != null) {+ ep = (!)glyph.new_point_on_path; + + begin_new_point_on_path = false;+ + // Fixa: även en ny punkt på objektet+ + glyph.add_new_edit_point (glyph.reverse_path_coordinate_x (ep.x), glyph.reverse_path_coordinate_x (ep.y));+ glyph.new_point_on_path = null;+ + MainWindow.get_toolbox ().select_tool_by_name ("new_point");++ return false;+ }+ + start_from_new_point_on_path (x, y);+ begin_new_point_on_path = true;+ + return false;+ } else {+ begin_new_point_on_path = false;+ }+ + if (glyph.active_point != null) {+ move_selected = true;+ return false;+ }+ + return true;+ }++ void start_from_new_point_on_path (double x, double y) { + Glyph g = MainWindow.get_current_glyph ();+ Path? p = null;+ Path pn;+ + p = g.get_closeset_path (x, y);++ pn = (!)p;++ if (pn.points.length () >= 2) {+ pn.set_editable (true);+ set_new_point_on_path (pn, 0, 0);+ g.open_path ();+ }+ + begin_new_point_on_path = false;+ }+ + public void begin_from_new_point_on_path () {+ begin_new_point_on_path = true;+ }++ private bool is_over_handle (double x, double y) {+ return selected_corner.get_left_handle ().get_point ().is_close (x, y) + || selected_corner.get_right_handle ().get_point ().is_close (x, y);+ }++ private void curve_corner_event (double x, double y) {+ EditPointHandle eh;+ + open_closest_path (x, y);+ + eh = selected_corner.get_left_handle ();+ + if (eh.get_point ().is_close (x, y)) {+ selected_handle = eh;+ move_selected_handle = true;+ }++ eh = selected_corner.get_right_handle ();+ + if (eh.get_point ().is_close (x, y)) {+ selected_handle = eh;+ move_selected_handle = true;+ }+ + }+ + /** Draw a test glyph. */+ public override bool test () {+ test_select_action ();+ + test_open_next_glyph ();+ + // paint+ test_click_action (1, 30, 30); + test_click_action (1, 60, 30);+ test_click_action (1, 60, 60);+ test_click_action (1, 30, 60);+ + // close+ test_click_action (3, 0, 0);++ // reopen+ test_click_action (3, 35, 35);+ + // move around+ test_move_action (100, 200);+ test_move_action (20, 300);+ test_move_action (0, 0);+ + // add to path+ test_move_action (70, 50);+ + test_click_action (1, 70, 50);+ test_click_action (1, 70, 50);+ test_click_action (1, 70, 100);+ test_click_action (1, 50, 100); + test_click_action (1, 50, 50);+ + // close+ test_click_action (3, 0, 0);+ + // merge it+ + this.yield ();+ + return true;+ }++ public void test_active_edit_point () {+ Glyph g;+ EditPoint epa, epb;+ + // paint+ test_select_action ();+ test_open_next_glyph ();++ g = MainWindow.get_current_glyph ();++ test_click_action (1, 130, 130); + epa = g.get_last_edit_point ();+ + test_click_action (1, 160, 130);+ test_click_action (1, 160, 160);+ epb = g.get_last_edit_point ();+ + test_click_action (1, 130, 160);+ + // validate active point+ test_move_action (130, 130);+ warn_if_fail (active_edit_point == epa);+ + test_move_action (161, 161); // close but not on+ warn_if_fail (active_edit_point == epb);+ + warn_if_fail (epa != epb);+ + // TODO: Test move handle here.+ }++ /** Test path coordinates and reverse path coordinates. */+ public void test_coordinates () {+ int x, y, xc, yc;+ double px, py;+ string n;+ + Glyph g = MainWindow.get_current_glyph ();+ + xc = (int) (g.allocation.width / 2.0);+ yc = (int) (g.allocation.height / 2.0);++ g.default_zoom ();+ + x = 10;+ y = 15;+ + px = g.path_coordinate_x (x);+ py = g.path_coordinate_y (y);++ test_reverse_coordinate (x, y, px, py, "ten fifteen");+ test_click_action (1, x, y);+ + // offset no zoom+ n = "Offset no zoom";+ g.reset_zoom ();+ + + px = g.path_coordinate_x (x);+ py = g.path_coordinate_y (y);+ + test_coordinate (px, py, x, y, n);+ test_reverse_coordinate (x, y, px, py, n);+ test_click_action (1, x, y);+ + // zoom + offset+ n = "View zoom";+ g.default_zoom ();+ + x = xc + 10;+ y = yc - 10;+ + px = g.path_coordinate_x (x);+ py = g.path_coordinate_y (y);+ + test_click_action (1, x, y);++ test_coordinate (px * g.view_zoom, py - yc * g.view_zoom, 10, 10, n);+ test_reverse_coordinate (x, y, px, py, n);+ + // close path+ test_click_action (3, x, y);+ }+ + private void test_coordinate (double x, double y, double px, double py, string n) {+ if (!(px - 0.5 <= x <= px + 0.5|| py - 0.5 <= y <= py + 0.5)) {+ warning (@"Expecting (x == px || y == py) got ($x == $px || $y == $py) in \"$n\"\n");+ }+ }+ + private void test_reverse_coordinate (int x, int y, double px, double py, string n) {+ Glyph g = MainWindow.get_current_glyph ();+ if (x != g.reverse_path_coordinate_x (px) || g.reverse_path_coordinate_y (py) != y) {+ warning (@"Reverse coordinates does not match current point for test case \"$n\".\n $x != $(g.reverse_path_coordinate_x (px)) || $(g.reverse_path_coordinate_y (py)) != $y (x != g.reverse_path_coordinate_x (px) || g.reverse_path_coordinate_y (py) != y)");+ }+ }+++}++}new file mode 100644@@ -0,0 +1,195 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/+namespace Supplement {++class Preferences {+ + static HashTable<string, string> data;++ public Preferences () {+ data = new HashTable<string, string> (str_hash, str_equal);+ }++ public static void set_last_file (string fn) {+ set ("last_file", fn);+ }++ public static string @get (string k) {+ string? s;+ + lock (data) {+ s = data.lookup (k);+ }+ + return (s != null) ? (!) s : "";+ }++ public static void @set (string k, string v) {+ lock (data) {+ data.replace (k, v);+ save (); // Fixa: save in separate io thread instead.+ }+ }++ public static string[] get_recent_files () {+ string recent = get ("recent_files");+ string[] files = recent.split ("\t");+ + for (uint i = 0; i < files.length; i++) {+ files[i] = files[i].replace ("\\t", "\t");+ }+ + return files;+ }++ public static bool is_ffi_file (string file) {+ // Fixa: Valabug: char_count deal with non-ascii letters+ // return (file.last_index_of (".ffi") != file.char_count () - 4);+ int i = file.last_index_of (".ffi");+ return (i > -1);+ }++ public static void add_recent_files (string file) {+ string escaped_string = file.replace ("\t", "\\t");+ StringBuilder recent = new StringBuilder ();++ if (!is_ffi_file (file)) {+ warning (@"\"$file\" is not a .ffi file.");+ return;+ }++ foreach (string f in get_recent_files ()) {+ if (f != file) {+ recent.append (f.replace ("\t", "\\t"));+ recent.append ("\t");+ }+ }++ recent.append (escaped_string);++ set ("recent_files", @"$(recent.str)");+ }++ public static int get_window_width() {+ string wp = get ("window_width");+ int w = int.parse (wp);+ return (w == 0) ? 360 : w;+ }++ public static int get_window_height() {+ int h = int.parse (get ("window_height"));+ return (h == 0) ? 500 : h;+ }+ + public static void update_window_size () {+ int w, h;+ MainWindow.get_current_window ().get_size (out w, out h);+ + set ("window_width", @"$w");+ set ("window_height", @"$h");+ }++ public static void load () {+ File app_dir = Supplement.get_settings_directory ();+ File settings = app_dir.get_child ("settings");++ data = new HashTable<string, string> (str_hash, str_equal);++ if (!settings.query_exists ()) {+ stderr.printf ("No settings file.\n");+ return;+ }++ FileStream? settings_file = FileStream.open ((!) settings.get_path (), "r");+ + if (settings_file == null) {+ stderr.printf ("Failed to load settings from file %s.\n", (!) settings.get_path ());+ return;+ }+ + return_if_fail (settings_file != null);+ + unowned FileStream b = (!) settings_file;+ + string? l;+ l = b.read_line ();+ while ((l = b.read_line ())!= null) {+ string line;+ + line = (!) l;+ + if (line.get_char (0) == '#') {+ continue;+ }+ + int i = 0;+ int s = 0;+ + i = line.index_of_char(' ', s);+ string key = line.substring (s, i - s);++ s = i + 1;+ i = line.index_of_char('"', s);+ s = i + 1;+ i = line.index_of_char('"', s);+ string val = line.substring (s, i - s);+ + // (key, val, description);+ + data.insert (key, val);+ }+ }+ + public static void save () {+ try {+ File app_dir = Supplement.get_settings_directory ();+ File settings = app_dir.get_child ("settings");++ return_if_fail (app_dir.query_exists ());+ + if (settings.query_exists ()) {+ settings.delete ();+ }++ DataOutputStream os = new DataOutputStream(settings.create(FileCreateFlags.REPLACE_DESTINATION));+ uint8[] d;+ long written = 0;+ + StringBuilder sb = new StringBuilder ();+ + sb.append ("# Supplement settings\n");+ sb.append ("# Version: 1.0\n");+ + foreach (var k in data.get_keys ()) {+ sb.append (k);+ sb.append (" \"");+ sb.append (data.lookup (k));+ sb.append ("\"\n");+ }+ + d = sb.str.data;+ + while (written < d.length) { + written += os.write (d[written:d.length]);+ }+ } catch (Error e) {+ stderr.printf ("Can not save key settings. (%s)", e.message); + } + }+}++}new file mode 100644@@ -0,0 +1,249 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;++namespace Supplement {++class ResizeTool : Tool {++ ImageSurface move_icon;+ ImageSurface move_icon_active;++ ImageSurface rotate_icon;+ ImageSurface rotate_icon_active;+ + int active_handle = -1;+ + Path selected_path = new Path ();+ + bool resize = false;+ double resize_begin_x = 0;+ double resize_begin_y = 0;+ Path? original_path = null;+ + public ResizeTool (string n) {+ base (n, "resize paths");++ select_action.connect((self) => {+ active_handle = -1;+ redraw_handle ();+ });+ + deselect_action.connect((self) => {+ active_handle = -1;+ redraw_handle ();+ });+ + press_action.connect((self, b, x, y) => {+ if (get_handle_at (x, y) > -1) {+ resize_begin_x = x;+ resize_begin_y = y;+ resize = true;+ original_path = null;+ }+ });++ release_action.connect((self, b, x, y) => {+ resize = false;+ original_path = null;+ });+ + move_action.connect ((self, x, y) => {+ high_light (x, y);+ + if (resize) {+ resize_process (x, y);+ }+ });+ + draw_action.connect ((self, cairo_context, glyph) => {+ draw_handle (cairo_context, glyph);+ });+ + move_icon = (!) Icons.get_icon ("move_handle.png");+ move_icon_active = (!) Icons.get_icon ("move_handle_active.png");++ rotate_icon = (!) Icons.get_icon ("rotate_icon.png");+ rotate_icon_active = (!) Icons.get_icon ("rotate_icon_active.png"); + + }++ private void resize_process (double x, double y) {+ double a, b, c, nc, tx, ty;+ + Path p;+ + // TODO: this function resizes only one path not a group of paths+ Path po = selected_path;+ + if (po.empty ()) return;+ + if (original_path == null) original_path = po.copy ();+ + p = ((!)original_path).copy ();+ + a = p.xmax - p.xmin;+ b = p.ymax - p.ymin;+ + tx = x - resize_begin_x;+ ty = y - resize_begin_y;+ + c = Math.sqrt (Math.pow (a, 2) + Math.pow (b, 2));+ nc = Math.sqrt (Math.pow (a + tx, 2) + Math.pow (b + ty, 2));++ p.resize (nc / c);+ po.replace_path (p);++ MainWindow.get_current_glyph ().queue_redraw_path (po);+ MainWindow.get_current_glyph ().queue_redraw_path (p);+ }++ private void high_light (double x, double y) {+ int i = get_handle_at (x, y);++ if (active_handle != i) {+ redraw_handle ();+ active_handle = i;+ }+ }++ private int get_handle_at (double x, double y) {+ Glyph g = MainWindow.get_current_glyph ();+ int hx, hy;+ double ivz = 1/g.view_zoom;+ + int i = 0;+ foreach (var p in g.path_list) {+ hx = (int) (p.xmax * ivz + g.view_offset_x + (g.allocation.width / 2) * ivz);+ hy = (int) (p.ymax * ivz + g.view_offset_y + (g.allocation.height / 2) * ivz);+ + if (p.points.length () > 0 && hx <= x <= hx + move_icon.get_width () && hy <= y <= hy + move_icon.get_height ()) {+ selected_path = p;+ return i;+ }+ + i++;+ }+ + selected_path = new Path ();+ return -1;+ }++ private void draw_handle (Context cr, Glyph g) {+ double x, y;+ int i = 0; + double ivz = 1/g.view_zoom;++ if (resize) return;+ + foreach (var p in g.path_list) {+ x = p.xmax * ivz + g.view_offset_x + (g.allocation.width / 2) * ivz;+ y = p.ymax * ivz + g.view_offset_y + (g.allocation.height / 2) * ivz;+ + if (p.points.length () > 0) {+ ImageSurface s = (i == active_handle) ? move_icon_active : move_icon;+ cr.set_source_surface (s, x, y);+ cr.paint ();++ ImageSurface r = (i == active_handle) ? rotate_icon_active : rotate_icon;+ cr.set_source_surface (r, x, y - 20);+ cr.paint ();+ }+ + i++;+ }+ }++ private void redraw_handle () {+ Glyph g = MainWindow.get_current_glyph ();+ int x, y;+ double ivz = 1/g.view_zoom;+ + foreach (var p in g.path_list) {+ x = (int) (p.xmax * ivz + g.view_offset_x + (g.allocation.width / 2) * ivz);+ y = (int) (p.ymax * ivz + g.view_offset_y + (g.allocation.height / 2) * ivz);++ g.queue_draw_area (x, y, (int) move_icon.get_width (), (int) move_icon.get_height ());+ g.queue_draw_area (0, 0, g.allocation.width, g.allocation.height);+ }+ }++ public override bool test () {+ // draw and resize glyphs + + draw_test_glyph (0, 100); // 1:1++ draw_test_glyph (0, 89); + resize_test_glyph (0.99); // 99%+ + for (int i = 2; i < 10; i++) {+ draw_test_glyph (10 * (i - 1), 100);+ resize_test_glyph (1.0 / i);+ }++ int m = 100;+ for (int i = 2; i < 6; i++) {+ m += ((i - 1) * 30) + 5;+ draw_test_glyph (0, m);+ resize_test_glyph (i);+ }++ this.yield ();+ + return true;+ }++ private void resize_test_glyph (double ratio_percent) {+ test_select_action ();+ + this.yield ();+ + Glyph g = MainWindow.get_current_glyph ();+ Path? pn = g.get_last_path ();+ return_if_fail (pn != null);++ Path p = (!) pn; + p.resize (ratio_percent);++ }+ + private void draw_test_glyph (int x_offset, int y_offset) {+ // open a new glyph+ Tool pen_tool = MainWindow.get_toolbox ().get_tool ("pen_tool");++ this.yield ();+ MainWindow.get_tab_bar ().select_overview ();++ this.yield ();+ MainWindow.get_overview ().open_current_glyph ();+ + pen_tool.test_select_action ();+ + // paint+ pen_tool.test_click_action (1, 10 + x_offset, 10 + y_offset); + pen_tool.test_click_action (1, 20 + x_offset, 10 + y_offset);+ pen_tool.test_click_action (1, 20 + x_offset, 20 + y_offset);+ pen_tool.test_click_action (1, 10 + x_offset, 20 + y_offset);+ + // close+ pen_tool.test_click_action (3, 0, 0);+ }+ +}++}new file mode 100644@@ -0,0 +1,253 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;+using Gdk;+using Cairo;++namespace Supplement {++class SaveDialog : FontDisplay {++ public signal void finished ();++ public Tool save = new Tool ("save");+ public Tool discard = new Tool ("no_icon");+ public Tool cancel = new Tool ("cancel_save");+ public Tool file_selection_dialog = new Tool ("file_selection");+ + FileSelection file_selection = new FileSelection ();++ int selected_index = 0;+ + public SaveDialog () {+ save.select_action.connect (() => {+ + if (file_selection.path.len < 4 || file_selection.path.str.substring (-4) != ".ffi") {+ file_selection.path.append (".ffi");+ }+ + bool s = Supplement.get_current_font ().save (file_selection.path.str);+ + Preferences.set_last_file (file_selection.path.str);+ Preferences.add_recent_files (file_selection.path.str);+ + if (s) {+ MainWindow.get_tab_bar ().close_display (this);+ finished ();+ } else {+ stderr.printf ("Failed to save $(file_selection.path.str)\n");+ }+ });++ discard.select_action.connect (() => {+ MainWindow.get_tab_bar ().close_display (this);+ finished ();+ });++ file_selection.redraw.connect (() => {+ redraw_area (file_selection.x - 50, file_selection.y - 50, 500, 500);+ });+ + cancel.select_action.connect (() => {+ MainWindow.get_tab_bar ().close_display (this);+ });+ + file_selection_dialog.select_action.connect (() => {+ show_file_selection ();+ });+ + save.set_default_color (1, 1, 1);+ discard.set_default_color (1, 1, 1);+ cancel.set_default_color (1, 1, 1);+ file_selection_dialog.set_default_color (1, 1, 1);+ }++ public override string get_name () {+ return "Save?";+ }+ + public override void draw (Allocation allocation, Context cr) {+ double xc, yc, d;+ + cr.save ();+ cr.set_line_width (0);+ cr.set_source_rgba (1, 1, 1, 1);+ cr.rectangle (0, 0, allocation.width, allocation.height);+ cr.fill_preserve ();+ + xc = allocation.width / 2.0;+ yc = allocation.height / 2.0;+ + cr.set_source_rgba (0/255.0, 0/255.0, 0/255.0, 1);++ d = 130;++ cr.move_to (xc - d, yc - 100);+ cr.set_font_size (16);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.NORMAL);+ cr.show_text ("Do you want to save your work?");+ + file_selection.x = xc - d;+ file_selection.y = yc - 70;+ file_selection.draw (allocation, cr);+ + if (selected_index == 0) save.set_active (true);+ if (selected_index == 1) discard.set_active (true);+ if (selected_index == 2) cancel.set_active (true);+ if (selected_index == 3) file_selection_dialog.set_active (true);+ + save.x = xc - d;+ save.y = yc - 40;+ save.draw (allocation, cr);++ discard.x = xc - d + 40;+ discard.y = yc - 40; + discard.draw (allocation, cr);++ cancel.x = xc - d + 80;+ cancel.y = yc - 40; + cancel.draw (allocation, cr);++ file_selection_dialog.x = xc - d + 120;+ file_selection_dialog.y = yc - 40; + file_selection_dialog.draw (allocation, cr);++ cr.restore ();++ cr.save ();+ + cr.restore ();+ }++ private void show_file_selection () {+ string? fn;+ Font font = Supplement.get_current_font ();+ FileChooserDialog file_chooser = new FileChooserDialog ("Save", MainWindow.get_current_window (), FileChooserAction.SAVE, Stock.CANCEL, ResponseType.CANCEL, Stock.SAVE, ResponseType.ACCEPT);+ file_chooser.set_current_folder_file (font.get_folder ());+ + if (file_chooser.run () == ResponseType.ACCEPT) { + MainWindow.get_glyph_canvas ().redraw ();+ + fn = file_chooser.get_filename ();++ if (fn != null) {+ file_selection.set_path ((!) fn);+ }+ }+ + file_chooser.destroy ();+ }+ + public override void selected_canvas () {+ }+ + public override void key_press (EventKey e) {+ if (e.type != EventType.KEY_PRESS) {+ return;+ }+ + if (e.keyval == Key.LEFT) {+ selected_index--;+ }++ if (e.keyval == Key.RIGHT) {+ selected_index++;+ }+ + if (e.keyval == Key.ENTER) {+ if (selected_index == 0) save.select_action (save);+ if (selected_index == 1) discard.select_action (discard);+ if (selected_index == 2) cancel.select_action (cancel);+ else file_selection_dialog.select_action (file_selection_dialog);+ }+ + file_selection.key_press (e);++ // keyboard moves selection+ save.set_active (false);+ discard.set_active (false);+ cancel.set_active (false);+ file_selection_dialog.set_active (false);+ + if (selected_index < 0) selected_index = 0;+ if (selected_index > 3) selected_index = 3;+ + if (selected_index == 0) save.set_active (true);+ if (selected_index == 1) discard.set_active (true);+ if (selected_index == 2) cancel.set_active (true);+ if (selected_index == 3) file_selection_dialog.set_active (true);+ + redraw_area (file_selection.x - 20, file_selection.y - 20, 500, 200);+ }+ + public override void key_release (EventKey e) {+ file_selection.key_release (e);+ }+ + public override void motion_notify (EventMotion e) {+ bool a, u;+ a = save.is_over (e.x, e.y);+ u = save.set_active (a);+ + a = discard.is_over (e.x, e.y);+ u = discard.set_active (a);++ a = cancel.is_over (e.x, e.y);+ u = cancel.set_active (a);++ a = file_selection_dialog.is_over (e.x, e.y);+ u = file_selection_dialog.set_active (a);+ + redraw_area (save.x - 10, save.y - 10, 200, 100);+ }+ + public override void button_release (EventButton e) {+ bool a;+ + a = save.is_over (e.x, e.y);+ if (a) {+ save.select_action (save);+ }++ a = discard.is_over (e.x, e.y);+ if (a) {+ discard.select_action (discard);+ }+ + a = cancel.is_over (e.x, e.y);+ if (a) {+ cancel.select_action (cancel);+ }+ + a = file_selection_dialog.is_over (e.x, e.y);+ if (a) {+ file_selection_dialog.select_action (file_selection_dialog);+ }+ + + }+ + public override void leave_notify (EventCrossing e) {+ }+ + public override void button_press (EventButton e) {+ }++}+ +}new file mode 100644@@ -0,0 +1,116 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;++namespace Supplement {++// TODO contrast++class ShrinkTool : Tool {++ public ShrinkTool (string n) {+ base (n, "shrink path");++ select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ + foreach (Path p in g.path_list) {+ shrink_copy (p, 0.25);+ }+ + MainWindow.get_glyph_canvas ().redraw ();+ });++ select_action.connect((self) => {+ });+ + press_action.connect((self, b, x, y) => {+ });++ move_action.connect((self, x, y) => {+ });+ }+ + public static int path_index (Path p) {+ Glyph g = MainWindow.get_current_glyph ();+ int i = 0;+ + foreach (Path pt in g.path_list) {+ if (p == pt) {+ return i;+ }+ i++;+ }+ + return -1;+ }+ + public static void shrink_copy (Path px, double step) { + Path p = px.copy ();+ shrink (px, step);+ p.set_editable (true);+ } + + public static void shrink (Path p, double step) { + if (p.points.length () < 3) return;++ p.create_list ();+ + EditPoint ep;++ p.create_list ();+ p.set_directions ();+ + foreach (EditPoint pep in p.points) {+ unowned List<EditPoint>? lep = pep.get_next ();+ + if (unlikely (lep == null)) {+ warning ("Bad list.");+ stderr.printf (@"No links in path $(path_index (p)) ($(p.get_name ())). Length: $(p.points.length ())\n");+ p.print_boundries ();+ continue;+ }+ + ep = ((!)lep).data;+ move_point (p, pep, ep, step);+ }+ + move_point (p, p.points.last ().data, p.points.first ().data, step);+ + p.set_editable (true);+ }+ + static void move_point (Path p, EditPoint pep, EditPoint ep, double step) { + double istep = 1 - step;+ + double mx = ep.get_right_handle ().x () / 2 + ep.get_left_handle ().x () / 2;+ double my = ep.get_right_handle ().y () / 2 + ep.get_left_handle ().y () / 2;+ + ep.x = mx * step + ep.x * istep;+ ep.y = my * step + ep.y * istep;++ }+ + public override bool test () {+ test_select_action ();+ return true;+ }++}+ +}new file mode 100644@@ -0,0 +1,190 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;++namespace Supplement {++class SpinButton : Tool {++ public signal void new_value_action (SpinButton selected);+ + public int8 deka = 2;+ public int8 deci = 0;+ public int8 centi = 0;+ public int8 milli = 0;+ + bool value_from_motion = false;+ double begin_x = 0;+ double begin_y = 0;+ double begin_value = 0;+ + public SpinButton (string? name = null, string tip = "", unichar key = '\0', uint modifier_flag = 0) {+ base (null , tip, key, modifier_flag);+ + if (name != null) {+ base.name = (!) name;+ }+ + set_icon ("spin_button");+ + panel_press_action.connect ((selected, button, tx, ty) => {+ double py = Math.fabs (y - ty);+ int n;+ + if (is_selected ()) {+ n = (button == 3) ? 10 : 1;+ + for (int i = 0; i < n; i++) {+ if (py < 28) increase ();+ if (py > 35) decrease ();+ }+ }+ + value_from_motion = true;+ begin_x = tx;+ begin_y = ty;+ + begin_value = double.parse (get_display_value ());+ });++ panel_move_action.connect ((selected, button, tx, ty) => {+ double d;+ if (value_from_motion) {+ d = (begin_y - ty) / 200;+ d = (d < 0) ? -Math.pow (d, 2) : Math.pow (d, 2);+ set_value_round (begin_value + d);+ }+ });++ panel_release_action.connect ((selected, button, tx, ty) => {+ value_from_motion = false;+ });+ }+ + public void increase () { + if (deka == 9 && deci == 9 && centi == 9 && milli == 9) {+ return;+ }+ + milli++;++ if (milli == 10) {+ centi++;+ milli = 0;+ }+ + if (centi == 10) {+ deci++;+ centi = 0;+ }++ if (deci == 10) {+ deka++;+ deci = 0;+ }+ + new_value_action (this);+ }++ public void decrease () {+ if (deka == 0 && deci == 0 && centi == 0) {+ return;+ }+ + milli--;+ + if (milli == -1) {+ centi--;+ milli = 9;+ }+ + if (centi == -1) {+ deci--;+ centi = 9;+ }++ if (deci == -1) {+ deka--;+ deci = 9;+ }+ + if (get_value () < 0.05) { // lower limit+ set_value_round (0.05);+ }+ + new_value_action (this);+ }++ public void set_value (string v) + requires (v.char_count () >= 5)+ {+ deka = (int8) int.parse (v.substring (0, 1));+ deci = (int8) int.parse (v.substring (2, 1));+ centi = (int8) int.parse (v.substring (3, 1));+ milli = (int8) int.parse (v.substring (4, 1));+ new_value_action (this);+ }++ public void set_value_round (double v) {+ int8 m;+ + v += 0.005;+ + if (v > 10) v = 9.999;+ if (v < 0.001) v = 0.001;+ + m = milli; // ignore milli value+ set_value (@"$v");+ milli = m;+ }+ + public double get_value () {+ return deka + (deci / 10.0) + (centi / 100.0) + (milli / 1000.0);+ }++ public string get_display_value () {+ return @"$deka.$deci$centi$milli";+ }++ public override void draw (Allocation allocation, Context cr) {+ double xt = x + 3 + w;+ double yt = y + h + 17;++ double text_x = -4.5;+ double text_y = 11;++ base.draw (allocation, cr);+ + cr.save ();+ + cr.set_source_rgba (0.8, 0.8, 0.8, 1);+ + cr.set_font_size (10);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.NORMAL);++ cr.move_to (xt + text_x, yt + text_y);+ + cr.show_text (get_display_value ());+ + cr.restore ();+ }+}++}new file mode 100644@@ -0,0 +1,219 @@+/* + Copyright (C) 2012 Johan Mattsson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +using Gtk; + +namespace Supplement { + +class Supplement { + + static Font current_font; + + static string[]? args = null; + + public static Font get_current_font () { + return current_font; + } + + public static void new_font () { + current_font = new Font (); + } + + public static File get_settings_directory () { + File home = File.new_for_path (Environment.get_home_dir ()); + File settings = home.get_child (".supplement"); + + if (!settings.query_exists ()) { + DirUtils.create ((!) settings.get_path (), 0xFFFFFF); + } + + return settings; + } + + public static File get_backup_directory () { + File settings = get_settings_directory (); + File backup = settings.get_child ("backup"); + + if (!backup.query_exists ()) { + DirUtils.create ((!) backup.get_path (), 0xFFFFFF); + } + + return backup; + } + + public static bool has_argument (string param) { + return (get_argument (param) != null); + } + + /** Get commandline argument. */ + public static string? get_argument (string param) + requires (args != null) + { + var a = (!) args; + int i = 0; + string? n; + + foreach (string s in a) { + string p; + + if (s.substring (0, 1) != "-") continue; + + if (s.substring (0, 2) != "--") { + p = expand_param (s); + } else { + p = s; + } + + if (param == p) { + if (i + 2 >= a.length) { + return ""; + } + + n = a[i + 2]; + if (n == null || ((!)n).length == 0) { + return ""; + } + + if (a[i + 2].substring (0, 1) == "-") { + return ""; + } + + return a[i + 2]; + } + + i++; + } + + return null; + } + + private static void print_padded (string cmd, string desc) { + int l = 25 - cmd.char_count (); + + stdout.printf (cmd); + + for (int i = 0; i < l; i++) { + stdout.printf (" "); + } + + stdout.printf (desc); + stdout.printf ("\n"); + } + + /** Return full command line parameter for an abbrevation. + * -t becomes --test. + */ + private static string expand_param (string? param) { + if (param == null) return ""; + var p = (!) param; + + if (p.length == 0) return ""; + if (p.get_char (0) != '-') return ""; + if (p.char_count () != 2) return ""; + + switch (p.get_char (1)) { + case 'a': + return "--autosave"; + case 'e': + return "--exit"; + case 's': + return "--slow"; + case 'h': + return "--help"; + case 't': + return "--test"; + } + + return ""; + } + + // FIXME: It seems broken: + public static string[] split_compound_parameters (string[] a) { + int ti = 0; + string[] na = new string [a.length]; + + foreach (string s in a) { + if (s.substring (0, 2) != "--" && s.length > 2) { + unichar c; + int t = 1; + + while (s.get_next_char (ref t, out c)) { + if (c != ' ' && c != '-') { + StringBuilder b = new StringBuilder (); + b.append_unichar (c); + na += @"-$(b.str)"; + } + } + } + + na[ti] = a[ti]; + ti++; + } + + return na; + } + + public static void print_help (string[] args) + requires (args.length > 0) + { + + stdout.printf ("Usage: "); + stdout.printf (args[0]); + stdout.printf (" [OPTION ...]\n"); + + print_padded ("-e, --exit", "exit if a testcase failes"); + print_padded ("-h, --help", "show this message"); + print_padded ("-s, --slow", "sleep between each command in test suite"); + print_padded ("-t, --test[=TEST]", "run test case"); + + stdout.printf ("\n"); + } + + public static void main(string[] args) { + Supplement.args = split_compound_parameters (args); + Supplement.args = args; + + if (has_argument ("--help")) { + print_help (args); + Process.exit(0); + } + + Preferences preferences = new Preferences (); + preferences.load (); + + current_font = new Font (); + + Gtk.init (ref args); + var window = new MainWindow ("Supplement"); + window.show_all (); + + var idle = new IdleSource (); + idle.set_callback(() => { + preferences.load (); + return false; + }); + + idle.attach (null); + + Gtk.main (); + + preferences.set_last_file (get_current_font ().get_path ()); + } + +} + +} new file mode 100644@@ -0,0 +1,311 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;++namespace Supplement {++class Svg {++ /** Export to svg glyph data. */+ public static string to_svg_glyph (Glyph g, double scale = 1) { + StringBuilder svg = new StringBuilder ();+ + foreach (Path pl in g.path_list) {+ if (pl.points.length () <= 2) {+ continue;+ }+ + pl.create_list ();+ + write_path_as_glyph (pl, svg, g, scale);+ }+ + return svg.str;+ }++ /** Export to regular svg path data. */+ public static string to_svg_path (Path pl, Glyph g, double scale = 1) { + StringBuilder svg = new StringBuilder ();+ pl.create_list ();+ write_path (pl, svg, g, false, scale);+ return svg.str;+ }++ private static void write_path_as_glyph (Path pl, StringBuilder svg, Glyph g, double scale = 1) + requires (pl.points.length () > 2)+ {+ write_path (pl, svg, g, true, scale);+ }++ private static void write_path (Path pl, StringBuilder svg, Glyph g, bool do_glyph, double scale = 1) + requires (pl.points.length () > 2)+ {+ int i = 0;+ EditPoint? n = null;+ EditPoint m;+ + foreach (var e in pl.points) {+ if (i == 0) {+ add_abs_start (e, svg, g, do_glyph, scale);+ i++;+ n = e;+ continue;+ }+ + m = (!) n;++ add_abs_next (m, e, svg, g, do_glyph, scale);+ + n = e;+ i++;+ }++ m = pl.points.first ().data;+ + add_abs_next ((!) n, m, svg, g, do_glyph, scale);++ close_path (svg);+ }++ private static void add_abs_next (EditPoint m, EditPoint e, StringBuilder svg, Glyph g, bool do_glyph, double scale = 1) {+ switch (m.type) {+ case PointType.LINE: + add_abs_line_to (e, m, svg, g, do_glyph, scale);+ break;+ + case PointType.CURVE:+ add_abs_path (e, m, svg, g, do_glyph, scale);+ break;+ + default:+ warning (@"PointType is $(e.type)");+ break;+ }+ }++ private static void add_abs_start (EditPoint ep, StringBuilder svg, Glyph g, bool do_glyph, double scale = 1) { + svg.append ("M");+ svg.append_printf ("0 0 ");+ }+ + private static void close_path (StringBuilder svg) {+ svg.append ("z");+ } + + private static void add_abs_line_to (EditPoint start, EditPoint stop, StringBuilder svg, Glyph g, bool to_glyph, double scale = 1) {+ double baseline = Supplement.get_current_font ().base_line;+ double left = g.left_limit;+ + double xa, ya, xb, yb;+ + Path.get_line_points (start, stop, out xa, out ya, out xb, out yb);++ double height = Supplement.get_current_font ().get_height (); // no probably not++ double center_x = Glyph.xc ();+ double center_y = Glyph.yc ();+ + if (!to_glyph) {+ print ("!to_glyph\n");+ svg.append ("L");+ + svg.append_printf ("%s ", round ((xa - center_x - left) * scale));+ svg.append_printf ("%s ", round ((ya - center_y - baseline + height) * scale)); ++ svg.append ("L");+ + svg.append_printf ("%s ", round ((xb - center_x - left) * scale));+ svg.append_printf ("%s ", round ((yb - center_y - baseline + height) * scale)); ++ } else {+ svg.append ("L");+ + svg.append_printf ("%s ", round ((xa - center_x - left) * scale));+ svg.append_printf ("%s ", round ((-ya + center_y + baseline) * scale));++ svg.append ("L");+ + svg.append_printf ("%s ", round ((xb - center_x - left) * scale));+ svg.append_printf ("%s ", round ((-yb + center_y + baseline) * scale)); + }+ }+ + private static void add_abs_path (EditPoint start, EditPoint end, StringBuilder svg, Glyph g, bool to_glyph, double scale = 1) {+ // Just like Path.print_curve (e, m, svg);+ + double left = g.left_limit;+ double baseline = Supplement.get_current_font ().base_line;+ double height = Supplement.get_current_font ().get_height (); // no probably not+ + double xa, ya, xb, yb, xc, yc, xd, yd;+ + Path.get_bezier_points (start, end, out xa, out ya, out xb, out yb, out xc, out yc, out xd, out yd);++ double center_x = Glyph.xc ();+ double center_y = Glyph.yc ();+ + if (!to_glyph) {+ svg.append_printf ("L");+ + svg.append_printf ("%s ", round ((xa - center_x - left) * scale));+ svg.append_printf ("%s ", round ((ya - center_y - baseline + height) * scale));+ + svg.append_printf ("C");++ svg.append_printf ("%s ", round ((xb - center_x - left) * scale));+ svg.append_printf ("%s ", round ((yb - center_y - baseline + height) * scale));+ + svg.append_printf ("%s ", round ((xc - center_x - left) * scale));+ svg.append_printf ("%s ", round ((yc - center_y - baseline + height) * scale));+ + svg.append_printf ("%s ", round ((xd - center_x - left) * scale));+ svg.append_printf ("%s ", round ((yd - center_y - baseline + height) * scale)); ++ } else {+ svg.append_printf ("L");+ + svg.append_printf ("%s ", round ((xa - center_x - left) * scale));+ svg.append_printf ("%s ", round ((-ya + center_y + baseline) * scale)); + + svg.append_printf ("C");++ svg.append_printf ("%s ", round ((xb - center_x - left) * scale));+ svg.append_printf ("%s ", round ((-yb + center_y + baseline) * scale));+ + svg.append_printf ("%s ", round ((xc - center_x - left) * scale));+ svg.append_printf ("%s ", round ((-yc + center_y + baseline) * scale)); + + svg.append_printf ("%s ", round ((xd - center_x - left) * scale));+ svg.append_printf ("%s ", round ((-yd + center_y + baseline) * scale)); + }+ }+ + /** Draw path from svg font data. */+ public static void draw_svg_path (Context cr, string svg, double x, double y, double scale = 1) {+ double x1, x2, x3;+ double y1, y2, y3;++ string[] d = svg.split (" ");+ + x /= scale;+ y /= scale;+ + cr.save ();++ cr.set_line_width (1);+ cr.set_source_rgba (0, 0, 0, 1);+ + if (svg == "") {+ return;+ }+ + for (int i = 0; i < d.length; i++) {+ + // trim off leading white space + while (d[i].index_of (" ") == 0) { + d[i] = d[i].substring (1);+ }+ + if (d[i].index_of ("L") == 0) {+ x1 = double.parse (d[i].substring (1)) + x;+ y1 = -double.parse (d[i+1]) + y;+ + x1 *= scale;+ y1 *= scale;+ + cr.line_to (x1, y1);+ continue;+ }++ if (d[i].index_of ("C") == 0) {+ x1 = double.parse (d[i].substring (1)) + x;+ y1 = -double.parse (d[i+1]) + y;++ x2 = double.parse (d[i+2]) + x;+ y2 = -double.parse (d[i+3]) + y;++ x3 = double.parse (d[i+4]) + x;+ y3 = -double.parse (d[i+5]) + y;++ x1 *= scale;+ y1 *= scale;++ x2 *= scale;+ y2 *= scale;+ + x3 *= scale;+ y3 *= scale;+ + cr.curve_to (x1, y1, x2, y2, x3, y3);+ continue;+ }++ if (d[i].index_of ("M") == 0) {+ x1 = double.parse (d[i].substring (1)) + x;+ y1 = -double.parse (d[i+1]) + y;+ + x1 *= scale;+ y1 *= scale;+ + cr.move_to (x1, y1);+ continue;+ }+ + if (d[i].index_of ("zM") == 0) {+ cr.close_path ();+ + x1 = double.parse (d[i].substring (2)) + x;+ y1 = -double.parse (d[i+1]) + y;+ + x1 *= scale;+ y1 *= scale;+ + cr.move_to (x1, y1);+ continue;+ }++ if (d[i].index_of ("z") == 0) {+ cr.close_path ();+ continue;+ }+ + }+ + cr.fill ();+ cr.restore ();+ }++}++public static string round (double p) {+ int t = (int) p;+ int u = (int) ((p - t) * 10);+ int v = (int) ((p - t - u / 10.0) * 100);+ int w = (int) ((p - t - u - v / 100.0) * 1000);++ if (u < 0) u = 0;+ if (v < 0) v = 0;+ if (w < 0) w = 0;++ StringBuilder sb = new StringBuilder ();+ sb.append_printf ("%d.%d%d%d", t, u, v, w);+ + return sb.str;+}+ +}new file mode 100644@@ -0,0 +1,116 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++class SvgFontFormatWriter : Object {++ DataOutputStream os;++ public SvgFontFormatWriter () {+ }++ public void open (File file) throws Error {+ if (file.query_exists ()) {+ throw new FileError.EXIST ("SvgFontFormatWriter: file exists.");+ }+ + os = new DataOutputStream (file.create(FileCreateFlags.REPLACE_DESTINATION));+ }++ public void close () throws Error {+ os.close ();+ }+ + public void write_font_file (Font font) throws Error {+ string font_name = font.get_name ();+ + int units_per_em = 100;+ + int ascent = 80;+ int descent = -20;+ + StringBuilder b;+ + Glyph? g;+ Glyph glyph;+ unichar indice = 0;+ + string uni;+ + put ("""<?xml version="1.0" standalone="no"?>""");+ put ("""<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >""");+ put ("""<svg xmlns="http://www.w3.org/2000/svg">""");++ // (metadata goes here)++ put ("<defs>");++ put (@"<font id=\"$font_name\" horiz-adv-x=\"250\" >");+ put (@"<font-face units-per-em=\"$units_per_em\" ascent=\"$ascent\" descent=\"$descent\" />");++ // (missing-glyph goes here)++ while (true) {+ g = font.get_glyph_indice (indice++);+ + if (g == null) {+ break;+ }+ + glyph = (!) g;+ + b = new StringBuilder ();+ b.append_unichar (glyph.get_unichar ());++ if (glyph.get_unichar () >= ' ' && b.str.validate ()) {+ if (b.str == "\"" || b.str == "&") {+ uni = Font.to_hex_code (glyph.get_unichar ());+ put (@"<glyph unicode=\"&#x$(uni);\" horiz-adv-x=\"$(glyph.get_width ())\" d=\"$(glyph.get_svg_data ())\" />"); + } else {+ put (@"<glyph unicode=\"$(b.str)\" horiz-adv-x=\"$(glyph.get_width ())\" d=\"$(glyph.get_svg_data ())\" />");+ }+ }+ }+ + font.kerning.for_each ((key, kern) => {+ try {+ string l = Font.to_hex_code (kern.left.get_char (0));+ string r = Font.to_hex_code (kern.right.get_char (0));+ + os.put_string (@"<hkern left=\"&#x$l;\" right=\"&#x$r;\" kerning=\"$(kern.val)\"/>\n");+ } catch (GLib.Error ef) {+ stderr.printf (@"Failed export font \n");+ stderr.printf (@"$(ef.message) \n");+ }+ });+ + put ("</font>");+ put ("</defs>");+ put ("</svg>");+ }++ /** Write a new line */+ private void put (string line) throws Error {+ os.put_string (line);+ os.put_string ("\n");+ }++}+++}new file mode 100644@@ -0,0 +1,402 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gdk;+using Gtk;++namespace Supplement {++class TabBar : DrawingArea {+ + public List<Tab> tabs = new List<Tab> ();++ int selected = 0;+ int over = -1;+ int over_close = -1;+ + public signal void signal_tab_selected (Tab selected_tab);++ Tab? previous_tab = null;+ Tab? current_tab = null;++ public TabBar () { + set_extension_events (ExtensionMode.CURSOR | EventMask.POINTER_MOTION_MASK);+ + add_events (EventMask.BUTTON_PRESS_MASK | EventMask.POINTER_MOTION_MASK | EventMask.LEAVE_NOTIFY_MASK);+ + motion_notify_event.connect ((t, e)=> {+ Allocation alloc;+ is_over_close (e.x, e.y, out over, out over_close);+ get_allocation (out alloc);+ queue_draw_area (0, 0, alloc.width, alloc.height);+ return true;+ }); + + leave_notify_event.connect ((t, e)=> {+ Allocation alloc;+ get_allocation (out alloc);+ over = -1;+ over_close = -1;+ queue_draw_area (0, 0, alloc.width, alloc.height);+ return true;+ });+ + button_press_event.connect ((t, e)=> {+ select_tab_click (e.x, e.y);+ return true;+ });+ + expose_event.connect ((t, e)=> {+ draw (e);+ return true;+ });+ + drag_begin.connect ((t, e)=> {+ stdout.printf("Drag.");+ });+ + drag_end.connect ((t, e)=> {+ stdout.printf("Drag end.");+ });+ + drag_motion.connect ((t, e, x, y, time)=> {+ stdout.printf("Drag motion.");+ return true; + });+ + drag_drop.connect ((t, e, x, y, time)=> {+ stdout.printf("Drag motion.");+ return true;+ }); + + set_size_request (20, 50);+ }+ + private void is_over_close (double x, double y, out int over, out int over_close) {+ int i = 0;+ double offset = 40;+ + foreach (Tab t in tabs) {+ + if ( offset < x < offset + t.get_width ()) {+ over = i;+ + if (15 < y < 50 - 15 && x > offset + t.get_width () - 16) {+ over_close = i;+ } else {+ over_close = -1;+ }+ + return;+ }+ + offset += t.get_width () + 10;+ i++;+ }++ over_close = -1; + over = -1;+ } + + + /** Select tab for a glyph by charcode or name.+ * @return true if the tab was found+ */+ public bool select_char (string s) {+ int i = 0;+ foreach (Tab t in tabs) {+ if (t.get_label () == s) {+ select_tab (i);+ return true;+ }+ i++;+ }+ + return false;+ }++ public bool select_tab_name (string s) {+ return select_char (s);+ }++ public void select_overview () {+ select_tab_name ("Overview");+ }++ private void select_previous_tab () {+ Tab t;+ bool open;+ + if (previous_tab == null) {+ return;+ }+ + t = (!) previous_tab;+ open = selected_open_tab (t);+ + if (!open) {+ select_tab ((int) tabs.length () - 1);+ }+ }+ + public void close_display (FontDisplay f) {+ int i = -1;+ foreach (var t in tabs) {+ ++i;+ + if (t.get_display () == f) {+ close_tab (i) ;+ return;+ }+ }+ + return_if_fail (i != -1);+ } + + public bool close_tab (int index, bool background_tab = false) {+ unowned List<Tab?>? lt;+ Tab t;+ Allocation alloc;+ get_allocation(out alloc);+ + if (!(0 <= index < tabs.length ())) {+ return false;+ }+ + lt = tabs.nth(index);+ + if (lt == null || ((!) lt).data == null) {+ return false;+ }++ t = (!) ((!) lt).data;++ if (t.has_close_button ()) {+ tabs.delete_link (tabs.nth(index));+ + if (!background_tab) {+ select_previous_tab ();+ }+ + return true;+ }+ + select_tab (index);+ return false;+ }+ + public bool close_by_name (string name, bool background_tab = false) {+ int i = 0;+ + foreach (var t in tabs) {+ if (t.get_label () == name) {+ return close_tab (i, background_tab);+ }+ + i++;+ }+ + return false;+ }+ + public void close_background_tab_by_name (string name) {+ close_by_name (name, true);+ }+ + /** Select a tab and return true if it is open. */+ public bool selected_open_tab (Tab t) {+ int i = 0;+ foreach (var n in tabs) {+ if (n == t) {+ select_tab (i);+ return true;+ }+ + i++;+ }+ + return false;+ }+ + public Tab get_selected_tab () {+ return tabs.nth (get_selected ()).data;+ }+ + public uint get_length () {+ return tabs.length ();+ }++ public int get_selected () {+ return selected;+ }+ + public void select_tab (int index)+ requires (0 <= index < tabs.length ())+ {+ Tab t;+ Allocation alloc;+ get_allocation(out alloc);+ + selected = index;+ + unowned List<Tab?>? lt = tabs.nth(index);+ + return_if_fail(lt != null);+ t = (!) ((!) lt).data;+ + signal_tab_selected (t);++ previous_tab = current_tab;+ current_tab = t;++ queue_draw_area(0, 0, alloc.width, alloc.height);+ }+ + private void select_tab_click (double x, double y) {+ int over, close;+ is_over_close (x, y, out over, out close);++ if (over >= 0) {+ if (over_close >= 0 && over == selected) {+ close_tab (over_close);+ } else {+ select_tab (over);+ }+ }+ }+ + public void add_tab (FontDisplay display_item, double tab_width = 30, bool always_open = false) {+ int s = (tabs.length () == 0) ? 0 : selected + 1;+ + tabs.insert (new Tab (display_item, tab_width, always_open), s);+ select_tab (s);+ }+ + /** Returns true if the new item was added to the bar. */+ public bool add_unique_tab (FontDisplay display_item, double tab_width = 30, bool always_open = false) {+ + bool i = select_tab_name (display_item.get_name ());+ + if (!i) {+ add_tab (display_item, tab_width, always_open);+ return true;+ }+ + return false;+ }+ + private void draw (EventExpose event) {+ Allocation alloc;+ Context cr = cairo_create (get_window ());+ + get_allocation (out alloc);+ + cr.save ();+ cr.rectangle (0, 0, alloc.width, alloc.height);+ cr.set_line_width (0);+ cr.set_source_rgba (183/255.0, 200/255.0, 223/255.0, 1);+ cr.fill_preserve ();+ cr.stroke ();+ cr.restore ();++ draw_tabs (cr);+ }+ + private void draw_tabs (Context cr) {+ Allocation alloc;+ get_allocation(out alloc);++ double close_opacity;+ double offset = 40;+ int i = 0;+ foreach (Tab t in tabs) {+ cr.save ();+ cr.translate (offset, 27);++ if (i == selected) {+ cr.set_source_rgba (1, 1, 1, 1);+ } else if (i == over) {+ cr.set_source_rgba (230/255.0, 236/255.0, 244/255.0, 1);+ } else {+ cr.set_source_rgba (142/255.0, 158/255.0, 190/255.0, 1);+ }+ + cr.rectangle(-2, 10, t.get_width (), 4);+ cr.set_line_width (0);+ cr.fill ();+ + cr.set_source_rgba (0, 0, 0, 1);+ cr.set_font_size (12);+ cr.move_to (0, 0);+ cr.show_text (t.get_label ());+ cr.stroke ();+ + // close+ if (t.has_close_button ()) {+ cr.set_line_width (1);+ + close_opacity = (over_close == i) ? 1 : 0.2; + cr.set_source_rgba (0, 0, 0, close_opacity);+ + cr.move_to (t.get_width () - 5, -6);+ cr.line_to (t.get_width () - 10, -1);++ cr.move_to (t.get_width () - 10, -6);+ cr.line_to (t.get_width () - 5, -1);+ + cr.stroke (); + }+ + cr.restore ();+ + offset += t.get_width () + 10; // FIXA 10 shoud be x off marg eller så+ i++;+ }+ }+}++class Tab : GLib.Object {++ bool always_open;+ double width; + FontDisplay display;++ public Tab (FontDisplay glyph, double tab_width, bool always_open) {+ width = tab_width;+ this.display = glyph;+ this.always_open = always_open;+ }++ public bool has_close_button () {+ return !always_open;+ }++ public FontDisplay get_display () {+ return display;+ }+ + public string get_label () {+ return display.get_name ();+ }+ + public double get_width () {+ return width;+ }+}++}new file mode 100644@@ -0,0 +1,339 @@+/* + Copyright (C) 2012 Johan Mattsson + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +namespace Supplement { + +class Test : Object { + public Callback callback; + public string name; + + public Test (Callback callback, string name) { + this.callback = callback; + this.name = name; + } +} + +/** All the things we might want to test is here. */ +class TestCases { + + public List<Test> test_cases; + + public TestCases () { + add (test_active_edit_point, "Active edit point"); + add (test_hex, "Unicode hex values"); + add (test_coordinates, "Coordinates"); + // add (test_merge_tool, "Merge paths"); + add (test_reverse_path, "Reverse path"); + add (test_reverse_random_paths, "Reverse random paths"); + add (test_drawing, "Pen tool"); + add (test_resize, "Resize tool"); + //add (test_export_tool, "Export tool"); + add (test_view_result, "View result in web browser"); + add (test_save_backup, "Save backup"); + + //add (test_tool_box, "Select all tools in Toolbox"); + } + + public static void test_tool_box () { + foreach (var exp in MainWindow.get_toolbox ().expanders) { + foreach (Tool t in exp.tool) { + MainWindow.get_toolbox ().select_tool (t); + } + } + + foreach (Tool t in MainWindow.get_content_display ().tools) { + t.select_action (t); + } + } + + public static void test_active_edit_point () { + PenTool tool = (PenTool) MainWindow.get_toolbox ().get_tool ("pen_tool"); + tool.test_active_edit_point (); + } + + public static void test_save_backup () { + // TODO draw various things and assert that they are restored correctly + Supplement.get_current_font ().save_backup (); + Supplement.get_current_font ().restore_backup (); + } + + public static void test_hex () { + test_hex_conv ('H', "U+48", 72); + test_hex_conv ('1', "U+31", 49); + test_hex_conv ('å', "U+e5", 229); + } + + private static void test_hex_conv (unichar h, string sr, int r) { + string s = Font.to_hex (h); + unichar t = Font.to_unichar (sr); + + if (s != sr) warning (@"($s != \"$sr\")"); + if ((int)t != r || t != h) warning (@"$((int)t) != $r || $t != '$h'"); + } + + public static void test_coordinates () { + PenTool tool = (PenTool) MainWindow.get_toolbox ().get_tool ("pen_tool"); + tool.test_coordinates (); + } + + public static void test_view_result () { + ExportTool tool = (ExportTool) MainWindow.get_toolbox ().get_tool ("export"); + tool.test_view_result (); + } + + public static void test_export_tool () { + Tool tool = MainWindow.get_toolbox ().get_tool ("export"); + tool.test (); + } + + public static void test_merge_tool () { + Tool tool = MainWindow.get_toolbox ().get_tool ("union_paths"); + tool.test (); + } + + public static void test_reverse_random_paths () { + MergeTool tool = (MergeTool) MainWindow.get_toolbox ().get_tool ("union_paths"); + tool.test_reverse_random_triangles (); + } + + public static void test_reverse_path () { + MergeTool tool = (MergeTool) MainWindow.get_toolbox ().get_tool ("union_paths"); + tool.test_reverse_path (); + } + + public static void test_drawing () { + Tool tool = MainWindow.get_toolbox ().get_tool ("pen_tool"); + tool.test (); + } + + public static void test_resize () { + Tool tool = MainWindow.get_toolbox ().get_tool ("resize"); + tool.test (); + } + + private void add (Callback callback, string name) { + test_cases.append (new Test (callback, name)); + } + + public unowned List<Test> get_test_functions () { + return test_cases; + } +} + +/** Class for executing tests cases. */ +class TestSupplement : GLib.Object { + + const int NOT_STARTED = 0; + const int RUNNING = 1; + const int PAUSED = 2; + const int DONE = 3; + + static int state = NOT_STARTED; + + TestCases tests; + + unowned List<Test> test_cases; + unowned List<Test?>? current_case = null; + + unowned List<Test> passed; + unowned List<Test> failed; + + static TestSupplement? singleton = null; + + bool has_failed = false; + + static bool slow_test = false; + + public TestSupplement () { + assert (singleton == null); + tests = new TestCases (); + test_cases = tests.get_test_functions (); + current_case = test_cases.first (); + + from_command_line (); + } + + public static bool is_slow_test () { + return slow_test; + } + + public static void set_slow_test (bool s) { + slow_test = s; + } + + public static TestSupplement get_singleton () { + if (singleton == null) { + singleton = new TestSupplement (); + } + + return (!) singleton; + } + + private bool has_test_case (string s) { + foreach (var t in test_cases) { + if (t.name == s) return true; + } + + if (s == "") { + print ("No speceific tescase given run all test cases.\n"); + return true; + } + + return false; + } + + /** Run only test specified on the command line. */ + private void from_command_line () { + string? stn = Supplement.get_argument ("--test"); + + if (stn != null) { + string st = (!)stn; + + if (!has_test_case (st)) { + stderr.printf (@"Test case $st does not exist.\n"); + stderr.printf ("\nAvaliable cases:\n"); + foreach (var t in test_cases) { + stderr.printf (t.name); + stderr.printf ("\n"); + } + + assert (false); + } + + if (st == "All" || st == "") { + return; + } else { + stderr.printf (@"Run only test case \"$st\" \n"); + } + + } + } + + public static void log (string? log_domain, LogLevelFlags log_levels, string message) { + Test t = (!)((!)get_singleton ().current_case).data; + + if (log_domain != null) { + stderr.printf ("%s: \n", (!) log_domain); + } + + stderr.printf ("Testcase \"%s\" failed because:\n", t.name); + stderr.printf ("%s\n\n", message); + + get_singleton ().has_failed = true; + } + + public static void @continue () { + int s = AtomicInt.get (ref state); + if (s == DONE) { + singleton = null; + } + + TestSupplement t = get_singleton (); + + LogLevelFlags levels = LogLevelFlags.LEVEL_ERROR | LogLevelFlags.LEVEL_CRITICAL | LogLevelFlags.LEVEL_WARNING; + Log.set_handler (null, levels, log); + + AtomicInt.set (ref state, RUNNING); + t.run_all_tests (); + } + + public static bool is_running () { + int r = AtomicInt.get (ref state); + return (r == RUNNING); + } + + public static void pause () { + AtomicInt.set (ref state, PAUSED); + Log.set_handler (null, LogLevelFlags.LEVEL_MASK, Log.default_handler); + } + + private static void pad (int t) { + for (int i = 0; i < t; i++) stdout.printf (" "); + } + + public void print_result () { + stdout.printf ("\n"); + stdout.printf ("Testcase results:\n"); + + foreach (var t in passed) { + stdout.printf ("%s", t.name); + pad (40 - t.name.char_count()); + stdout.printf ("Passed\n"); + } + + foreach (var t in failed) { + stdout.printf ("%s", t.name); + pad (40 - t.name.char_count()); + stdout.printf ("Failed\n"); + } + + stdout.printf ("\n"); + + stdout.printf ("Total %u test cases executed, %u passed and %u failed.\n", (passed.length () + failed.length ()), passed.length (), failed.length ()); + } + + /** Run tests in main loop. */ + public void run_all_tests () { + var idle = new TimeoutSource (20); + + idle.set_callback(() => { + int s = AtomicInt.get (ref state); + + if (s != RUNNING || current_case == null) { + return false; + } + + Test test = (!)((!) current_case).data; + + has_failed = false; + + test.callback (); + + if (has_failed) { + failed.append ((!)test); + + if (Supplement.has_argument ("--exit")) { + print_result (); + + bool test_case_will_not_fail = false; + assert (test_case_will_not_fail); + } + + } else { + passed.append ((!)test); + } + + if (unlikely (current_case == test_cases.last ())) { + stderr.printf ("Finished running test suite.\n"); + AtomicInt.set (ref state, DONE); + Log.set_handler (null, LogLevelFlags.LEVEL_MASK, Log.default_handler); + + print_result (); + return false; + } + + current_case = ((!) current_case).next; + + return true; + }); + + idle.attach (null); + } + +} + +} new file mode 100644@@ -0,0 +1,326 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;++namespace Supplement {++class Tool : GLib.Object {+ + public double x = 0;+ public double y = 0;+ public double w = 0;+ public double h = 0;++ protected bool active = false;+ protected bool selected = false;+ + double r_default_selected = 50/255.0;+ double g_default_selected = 50/255.0;+ double b_default_selected = 50/255.0;+ + double r_default = 82/255.0;+ double g_default = 82/255.0;+ double b_default = 82/255.0;+ + double r;+ double g;+ double b;++ ImageSurface? icon = null;+ + public signal void select_action (Tool selected);+ public signal void deselect_action (Tool selected);+ + public signal void press_action (Tool selected, int button, int x, int y);+ public signal void move_action (Tool selected, int x, int y);+ public signal void release_action (Tool selected, int button, int x, int y);+ + public signal void panel_press_action (Tool selected, uint button, double x, double y);+ public signal void panel_release_action (Tool selected, uint button, double x, double y);+ public signal void panel_move_action (Tool selected, double x, double y);+ + public signal void draw_action (Tool selected, Context cr, Glyph glyph);+ + protected string name = "";+ + static int next_id = 1;+ + int id;+ + public bool new_selection = false;+ + bool show_bg = true;+ + string tip = "";++ // keyboard bindings+ public uint modifier_flag;+ public unichar key;+ + /** Create tool with a certain name and load icon "name".png */+ public Tool (string? name = null, string tip = "", unichar key = '\0', uint modifier_flag = 0) {+ this.tip = tip;+ + if (name != null) {+ set_icon ((!) name);+ this.name = (!) name;+ }+ + this.key = key;+ this.modifier_flag = modifier_flag;+ + id = next_id;+ next_id++;+ + panel_press_action.connect ((self, button, x, y) => {+ MainWindow.get_tool_tip ().set_text_from_tool ();+ });+ + r = r_default;+ g = g_default;+ b = b_default;+ }+ + public void set_default_color (double r, double g, double b) {+ r_default = r;+ g_default = g;+ b_default = b;+ + this.r = r;+ this.g = g;+ this.b = b;+ }+ + public void set_icon (string name) {+ StringBuilder n = new StringBuilder ();+ n.append ((!) name);+ n.append (".png");+ icon = Icons.get_icon (n.str);+ }+ + public bool is_active () {+ return active;+ }+ + public void set_show_background (bool bg) {+ show_bg = bg;+ }+ + public int get_id () {+ return id;+ }++ public string get_name () {+ return name;+ }++ public bool is_selected () {+ return selected;+ }+ + public virtual void set_properties (double tx, double ty, double tw, double th) {+ x = tx;+ y = ty;+ w = tw;+ h = th;+ }++ public string get_tip () {+ return tip;+ }++ public bool is_over (double xp, double yp) { + return (x <= xp <= x + w + 12 + 15 && y + 15 <= yp <= y + w + 12 + 30); + }+ + public bool set_selected (bool a) {+ TooltipArea? tpa = null; + tpa = MainWindow.get_tool_tip ();+ + new_selection = true;+ selected = a;+ set_active (a);+ + if (!a) {+ deselect_action (this);+ } else {+ ((!)tpa).update_text ();+ }+ + return true;+ }+ + public bool set_active (bool a) {+ bool ret = (active != a);++ if (selected) {+ if (a) {+ r = r_default_selected * 0.9;+ g = r_default_selected * 0.9;+ b = r_default_selected * 0.9;+ } else {+ r = r_default_selected;+ g = g_default_selected;+ b = b_default_selected;+ } + } else {+ if (a) {+ r = r_default * 0.9;+ g = r_default * 0.9;+ b = r_default * 0.9;+ } else {+ r = r_default;+ g = g_default;+ b = b_default;+ }+ }++ active = a;+ return ret;+ }+ + public virtual void draw (Allocation allocation, Context cr) {+ double xt = x + 3 + w;+ double yt = y + h + 17;++ cr.save ();++ // Buttons+ if (show_bg || icon == null || active || selected) {+ cr.set_line_join (LineJoin.ROUND);+ cr.set_line_width (12);+ cr.set_source_rgba (r, g, b, 1);+ cr.rectangle (xt, yt, 18, 15);+ cr.fill_preserve ();+ cr.stroke ();+ }+ + if (icon != null) {+ ImageSurface i = (!) icon;+ + if (likely (i.status () == Cairo.Status.SUCCESS)) {+ cr.set_source_surface (i, xt + (15 - i.get_width () / 2) - 5.7, yt + (15 - i.get_height ()) / 2.0);+ cr.paint ();+ } else {+ warning (@"Falied to load icon for $name");+ }+ }+ + cr.restore ();+ }++ /** Run a test case for this tool. */+ public virtual bool test () {+ stderr.printf (@"$(get_name ()) does not have a test case, implement one by overriding test method in Tool base class.");+ return false;+ }+ + /** Help function to test button press actions. */+ public void test_click_action (int b, int x, int y) {+ this.yield ();+ press_action (this, b, x, y);+ + this.yield ();+ release_action (this, b, x, y);+ }++ /** Help function to test select action for this tool. */+ public void test_select_action () {+ this.yield ();+ MainWindow.get_toolbox ().select_tool (this);+ }++ public void test_move_action (int x, int y) {+ this.yield ();+ move_action (this, x, y);+ }++ public void test_press_action (int b, int x, int y) {+ this.yield ();+ press_action (this, b, x, y);+ }++ public void test_release_action (int b, int x, int y) {+ this.yield ();+ release_action (this, b, x, y);+ }++ public void test_open_next_glyph () {+ this.yield ();+ OverView o = MainWindow.get_overview ();+ + MainWindow.get_tab_bar ().select_overview ();+ + o.select_next_glyph ();+ this.yield ();+ + o.open_current_glyph ();+ this.yield ();+ }+ + /** Run pending events in main loop before continue. */+ public void @yield () {+ int t = 0;+ var time = new TimeoutSource(500);+ bool timeout;++ if (TestSupplement.is_slow_test ()) {+ timeout = false;+ + time.set_callback(() => {+ timeout = true;+ return false;+ });++ time.attach(null); + } else {+ timeout = true;+ }+ + unowned MainContext c = MainContext.default ();+ bool a = c.acquire ();+ + while (unlikely (!a)) {+ warning ("Failed to acquire main loop.\n");+ a = c.acquire ();+ Thread.usleep (2*1000000);+ }++ while (c.pending () || TestSupplement.is_slow_test ()) {+ c.iteration (true);+ t++;++ if (!c.pending () && TestSupplement.is_slow_test ()) {+ if (timeout) break;+ }+ + if (unlikely (t > 100000)) {+ warning (@"\nToo many interations in test suite main loop for $(get_name ())\n");+ assert (false);+ break;+ }+ }+ + c.release ();+ }+ +}++}new file mode 100644@@ -0,0 +1,1177 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;+using Gdk;+using Cairo;++namespace Supplement {++class Toolbox : DrawingArea {+ GlyphCanvas glyph_canvas;+ + bool expanded = true;+ + ToolboxExpander toolbox_expander;+ + public List<Expander> expanders;+ + Tool current_tool = new Tool ("no_icon");+ + Expander draw_tools;+ Expander grid_expander;+ + BackgroundTool move_background;+ CutBackgroundTool cut_background;+ + Tool press_tool = new Tool (null); // activate the pressed button on release+ + public SpinButton background_scale = new SpinButton ();+ + public Toolbox (GlyphCanvas main_glyph_canvas) {+ glyph_canvas = main_glyph_canvas;+ + set_size_request (158, 100);++ leave_notify_event.connect ((t, e)=> {+ reset_active_tool ();+ return true;+ });+ + toolbox_expander = new ToolboxExpander ();+ + add_expander (toolbox_expander);+ + draw_tools = new Expander ();+ + Expander path_tool_modifiers = new Expander ();+ Expander draw_tool_modifiers = new Expander ();+ Expander characterset_tools = new Expander ();+ Expander test_tools = new Expander ();+ Expander guideline_tools = new Expander ();+ Expander view_tools = new Expander ();+ Expander grid = new Expander ();+ Expander background_tools = new Expander ();+ Expander trace = new Expander ();+ + grid_expander = grid;++ // Draw tools+ PenTool pen_tool = new PenTool ("pen_tool");+ draw_tools.add_tool (pen_tool);+ + ZoomTool zoom_tool = new ZoomTool ("zoom_tool");+ draw_tools.add_tool (zoom_tool);++ CutTool cut_tool = new CutTool ("cut");+ draw_tools.add_tool (cut_tool); ++ Tool move_tool = new MoveTool ("move");+ draw_tools.add_tool (move_tool); ++ Tool resize_tool = new ResizeTool ("resize");+ // Fixa: draw_tools.add_tool (resize_tool); ++ Tool shrink_tool = new ShrinkTool ("shrink");+ // Fixa: draw_tools.add_tool (shrink_tool); + + // Draw tool modifiers+ Tool new_point = new Tool ("new_point", "Add new points to path", 'a');+ new_point.select_action.connect((self) => {+ select_draw_tool ();+ });+ + draw_tool_modifiers.add_tool (new_point);++ Tool corner_tool = new Tool ("corner", "Edit corner", 'e');+ corner_tool.select_action.connect((self) => {+ select_draw_tool ();+ pen_tool.edit_active_corner = true;+ });+ corner_tool.deselect_action.connect((self) => {+ pen_tool.edit_active_corner = false;+ });+ draw_tool_modifiers.add_tool (corner_tool);++ Tool new_point_on_path = new Tool ("new_point_on_path", "Add new point on path", 'n');+ new_point_on_path.select_action.connect((self) => {+ select_draw_tool ();+ pen_tool.begin_from_new_point_on_path ();+ });+ draw_tool_modifiers.add_tool (new_point_on_path);++ Tool move_point_on_path = new Tool ("move_point", "Move edit point", 'm');+ move_point_on_path.select_action.connect((self) => {+ select_draw_tool ();+ pen_tool.move_point_on_path = true;+ });+ corner_tool.deselect_action.connect((self) => {+ pen_tool.move_point_on_path = false;+ });+ draw_tool_modifiers.add_tool (move_point_on_path); ++ Tool close_paths_tool = new Tool ("close_paths", "Open or close paths", 'c');+ close_paths_tool.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ + select_draw_tool ();+ + // Set direction for inside or outside+ cut_tool.force_direction ();+ + if (!g.close_path ()) {+ g.open_path ();+ }+ + TimeoutSource anim = new TimeoutSource (300);+ anim.set_callback(() => {+ select_tool (new_point);+ return false;+ });+ + anim.attach (null);+ + });+ draw_tool_modifiers.add_tool (close_paths_tool); ++ Tool tie_editpoint_tool = new Tool ("tie_point", "Tie curve handles for selected edit point", 'w');+ tie_editpoint_tool.select_action.connect((self) => {+ EditPoint ep;+ EditPointHandle eh;+ + select_draw_tool ();+ + ep = pen_tool.selected_corner;+ + ep.tie_handles = !ep.tie_handles;+ + eh = (ep.left_handle.length > ep.right_handle.length) ? ep.left_handle : ep.right_handle;+ + if (ep.next != null) {+ eh.move_to_coordinate (eh.x (), eh.y ());+ ep.type = PointType.CURVE;+ + ep.get_next ().data.set_point_type (PointType.CURVE); // Fixa: add curve left, right and both+ ep.get_next ().data.set_point_type (PointType.CURVE);+ }+ + TimeoutSource anim = new TimeoutSource (300);+ anim.set_callback(() => {+ select_tool (corner_tool);+ return false;+ });+ + anim.attach (null);+ + });+ draw_tool_modifiers.add_tool (tie_editpoint_tool); ++ Tool tie_x_or_y = new Tool ("tie_x_or_y", "Tie coordinates to previous edit point", 'q');+ tie_x_or_y.select_action.connect((self) => {+ select_draw_tool ();+ pen_tool.tie_x_or_y_coordinates = !pen_tool.tie_x_or_y_coordinates;+ });+ // Fixa: draw_tool_modifiers.add_tool (tie_x_or_y); ++ Tool erase_tool = new Tool ("erase_tool", "Delete edit points");+ erase_tool.select_action.connect((self) => {+ select_draw_tool ();+ });+ draw_tool_modifiers.add_tool (erase_tool); + + // path tools+ Tool union_paths_tool = new MergeTool ("union_paths");+ // path_tool_modifiers.add_tool (union_paths_tool);+ // Fixa: write merge tool+ + Tool reverse_path_tool = new Tool ("reverse_path", "Create counter from outline", 'r');+ reverse_path_tool.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ Path? p = g.active_path;+ + if (p != null) {+ ((!)p).reverse ();+ }+ + g.redraw_area (0, 0, g.allocation.width, g.allocation.height);+ });+ path_tool_modifiers.add_tool (reverse_path_tool);++ Tool move_layer = new Tool ("move_layer", "Move to path to bottom layer", 'd');+ move_layer.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ unowned List <Path> pl; + Path p;+ + if (g.active_path != null) {+ p = (!) g.active_path;+ + g.path_list.remove (p);+ g.path_list.prepend (p);+ }+ });+ path_tool_modifiers.add_tool (move_layer);+ + // Character set tools+ Tool full_unicode = new Tool ("utf_8", "Show full unicode characters set", 'a', CTRL);+ full_unicode.select_action.connect((self) => {+ MainWindow.get_tab_bar ().add_unique_tab (new OverView (), 75, false); + OverView o = MainWindow.get_overview ();+ GlyphRange gr = new GlyphRange ();+ gr.use_full_unicode_range ();+ o.set_glyph_range (gr);+ MainWindow.get_tab_bar ().select_tab_name ("Overview");+ });+ characterset_tools.add_tool (full_unicode);++ Tool custom_character_set = new Tool ("custom_character_set", "Show default characters set", 'r', CTRL);+ custom_character_set.select_action.connect((self) => {+ MainWindow.get_tab_bar ().add_unique_tab (new OverView (), 75, false);+ OverView o = MainWindow.get_overview ();+ GlyphRange gr = new GlyphRange ();+ gr.use_default_range ();+ o.set_glyph_range (gr);+ MainWindow.get_tab_bar ().select_tab_name ("Overview");+ });+ characterset_tools.add_tool (custom_character_set);++ Tool avalilable_characters = new Tool ("available_characters", "Show characters in font", 'd', CTRL);+ avalilable_characters.select_action.connect((self) => {+ MainWindow.get_tab_bar ().add_unique_tab (new OverView (), 75, false);+ OverView o = MainWindow.get_overview ();+ Font f = Supplement.get_current_font ();+ GlyphRange gr = f.get_available_glyph_ranges ();+ o.set_glyph_range (gr);+ MainWindow.get_tab_bar ().select_tab_name ("Overview");+ });+ characterset_tools.add_tool (avalilable_characters);++ Tool delete_glyph = new Tool ("delete_selected_glyph", "Delete selected glyph or path");+ delete_glyph.select_action.connect((self) => {+ OverView o = MainWindow.get_overview ();+ + if (MainWindow.get_current_display () is OverView) {+ o.delete_selected_glyph ();+ }+ + MainWindow.get_tab_bar ().select_tab_name ("Overview");+ });+ characterset_tools.add_tool (delete_glyph);+ + if (Supplement.has_argument ("--test")) {+ Tool test_case = new Tool ("test_case");+ test_case.select_action.connect((self) => {+ if (self.is_selected ()) {+ if (TestSupplement.is_running ()) {+ TestSupplement.pause ();+ } else {+ TestSupplement.continue ();+ }+ }+ });+ test_tools.add_tool (test_case);++ Tool slow_test = new Tool ("slow_test");+ slow_test.select_action.connect((self) => {+ bool s = TestSupplement.is_slow_test ();+ TestSupplement.set_slow_test (!s);+ s = TestSupplement.is_slow_test ();+ self.set_selected (s);+ });+ + test_tools.add_tool (slow_test);+ + // Run from commad line+ string? st = Supplement.get_argument ("--test");+ if (st != null && ((!)st).char_count () > 0) {+ IdleSource idle = new IdleSource ();++ idle.set_callback (() => { + select_tool (test_case);+ return false;+ });+ + idle.attach (null);+ }+ + if (Supplement.has_argument ("--slow")) {+ select_tool (slow_test);+ }+ + }+ + // help lines, grid and other guidlines+ Tool help_lines = new Tool ("help_lines", "Show help lines", 'l');+ help_lines.select_action.connect((self) => {+ bool h;+ h = glyph_canvas.get_current_glyph ().get_show_help_lines ();+ glyph_canvas.get_current_glyph ().set_show_help_lines (!h);+ self.set_selected (!h);+ glyph_canvas.get_current_glyph ().redraw_help_lines ();+ }); ++ guideline_tools.add_tool (help_lines);++ Tool xheight_help_lines = new Tool ("show_xheight_helplines", "Show help lines for x-height and baseline", 'x');+ xheight_help_lines.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ bool v = !g.get_xheight_lines_visible ();+ g.set_xheight_lines_visible (v);+ self.set_selected (v);+ MainWindow.get_glyph_canvas ().redraw ();+ + if (v && !help_lines.is_selected ()) {+ select_tool (help_lines);+ }+ + });+ guideline_tools.add_tool (xheight_help_lines);++ Tool background_help_lines = new Tool ("background_help_lines", "Show help lines at top and bottom margin", 't');+ background_help_lines.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ bool v = !g.get_margin_lines_visible ();+ g.set_margin_lines_visible (v);+ self.set_selected (v);+ MainWindow.get_glyph_canvas ().redraw ();+ + if (v && !help_lines.is_selected ()) {+ select_tool (help_lines);+ }+ });+ guideline_tools.add_tool (background_help_lines);++ Tool new_grid = new GridTool ("new_grid");+ guideline_tools.add_tool (new_grid);++ // Zoom tools + Tool zoom_in = new Tool ("zoom_in", "zoom in", '+', CTRL);+ zoom_in.select_action.connect((self) => {+ zoom_tool.store_current_view ();+ glyph_canvas.get_current_display ().zoom_in ();+ });+ view_tools.add_tool (zoom_in);++ Tool zoom_out = new Tool ("zoom_out", "Zoom out", '-', CTRL);+ zoom_out.select_action.connect((self) => {+ zoom_tool.store_current_view ();+ glyph_canvas.get_current_display ().zoom_out ();+ });+ view_tools.add_tool (zoom_out);++ Tool reset_zoom = new Tool ("zoom_1_1", "Zoom to scale 1:1", '0', CTRL);+ reset_zoom.select_action.connect((self) => {+ zoom_tool.store_current_view ();+ glyph_canvas.get_current_display ().reset_zoom ();+ glyph_canvas.queue_draw_area(0, 0, glyph_canvas.allocation.width, glyph_canvas.allocation.height);+ });+ view_tools.add_tool (reset_zoom);++ Tool full_glyph = new Tool ("full_glyph", "Show full glyph", 'f');+ full_glyph.select_action.connect((self) => {+ zoom_tool.store_current_view ();+ zoom_tool.zoom_full_glyph ();+ });+ view_tools.add_tool (full_glyph);++ Tool zoom_boundries = new Tool ("zoom_boundries", "Zoom in at region boundries", 'v');+ zoom_boundries.select_action.connect((self) => {+ zoom_tool.store_current_view ();+ glyph_canvas.get_current_display ().zoom_max ();+ });+ view_tools.add_tool (zoom_boundries);++ Tool zoom_bg = new Tool ("zoom_background_image", "Zoom in background image", 'b');+ zoom_bg.select_action.connect((self) => {+ if (MainWindow.get_current_glyph ().get_background_image () != null) {+ zoom_tool.store_current_view (); + glyph_canvas.get_current_display ().reset_zoom ();+ + zoom_tool.zoom_full_background_image ();+ + glyph_canvas.queue_draw_area(0, 0, glyph_canvas.allocation.width, glyph_canvas.allocation.height);+ }+ });+ view_tools.add_tool (zoom_bg);++ Tool zoom_prev = new Tool ("prev", "previous view", 'p', CTRL);+ zoom_prev.select_action.connect((self) => {+ zoom_tool.previous_view ();+ });+ view_tools.add_tool (zoom_prev);++ Tool zoom_next = new Tool ("next", "Next view", 'l', CTRL);+ zoom_next.select_action.connect((self) => {+ zoom_tool.next_view ();+ });+ view_tools.add_tool (zoom_next);+ + // background tools+ background_scale = new SpinButton ("scale_background", "Set size for background image");+ background_scale.set_value_round (1);+ + background_scale.new_value_action.connect((self) => {+ background_scale.select_action (self);+ });+ + background_scale.select_action.connect((self) => {+ SpinButton sb = (SpinButton) self;+ Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? img = g.get_background_image ();+ double s = sb.get_value ();+ + if (MainWindow.get_current_display () is CutBackgroundCanvas) {+ return;+ }+ + if (img != null) {+ ((!)img).set_img_scale (s, s);+ }+ + MainWindow.get_glyph_canvas ().redraw ();+ });+ + background_tools.add_tool (background_scale); ++ BackgroundTool move_background = new BackgroundTool ("move_background"); + background_tools.add_tool (move_background);+ this.move_background = move_background;+ + CutBackgroundTool cut_background = new CutBackgroundTool ("cut_background");+ background_tools.add_tool (cut_background);+ this.cut_background = cut_background;+ + Tool show_bg = new Tool ("show_background", "Show background image", 'F', SHIFT);+ show_bg.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ g.set_background_visible (!g.get_background_visible ());+ MainWindow.get_glyph_canvas ().redraw ();+ });+ background_tools.add_tool (show_bg);++ Tool background_size_to_boundries = new Tool ("size_to_boundries", "Fit image between margin lines", 'F', SHIFT);+ background_size_to_boundries.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? bg = g.get_background_image ();+ GlyphBackgroundImage b = (!) bg;+ + if (bg != null) {+ + if (!b.scaled) {+ b.scale_image_proportional_to_boundries (g);+ g.set_background_visible (true);+ MainWindow.get_glyph_canvas ().redraw ();+ } else {+ b.reset_scale (g);+ g.set_background_visible (true);+ MainWindow.get_glyph_canvas ().redraw (); + }+ + b.scaled = !b.scaled;+ }+ });+ // Fixa: remove this tool background_tools.add_tool (background_size_to_boundries);++ Tool bg_selection = new Tool ("insert_background", "Insert new background image");+ bg_selection.select_action.connect((self) => {+ Glyph? g = null;+ FontDisplay fd = MainWindow.get_current_display ();+ TooltipArea tp = MainWindow.get_tool_tip ();+ tp.show_text ("Creating thumbnails");+ + bg_selection.yield ();+ + if (fd is Glyph) {+ g = (Glyph) fd;+ } else {+ return;+ }+ + BackgroundSelection bgs = new BackgroundSelection ((!) g);+ MainWindow.get_tab_bar ().add_unique_tab (bgs , 120, false);+ + tp.show_text ("");+ });+ bg_selection.set_show_background (true);+ background_tools.add_tool (bg_selection);+ + SpinButton background_contrast = new SpinButton ("background_contrast", "Set background contrast");+ background_contrast.set_value_round (1);++ background_contrast.new_value_action.connect((self) => {+ background_contrast.select_action (self);+ });+ + background_contrast.select_action.connect((self) => { + Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? bg = g.get_background_image ();+ GlyphBackgroundImage b;+ + if (bg != null) {+ b = (!) bg;+ b.set_contrast (background_contrast.get_value ());+ }+ + });+ + // Fixa: background_tools.add_tool (background_contrast);++ SpinButton background_threshold = new SpinButton ("background_threshold", "Set threshold");+ background_threshold.set_value_round (1);++ background_threshold.new_value_action.connect((self) => {+ background_threshold.select_action (self);+ });+ + background_threshold.select_action.connect((self) => { + Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? bg = g.get_background_image ();+ GlyphBackgroundImage b;+ + if (bg != null) {+ b = (!) bg;+ b.set_threshold (background_threshold.get_value ());+ }+ + });+ trace.add_tool (background_threshold);++ Tool auto_trace = new Tool ("auto_trace", "Auto trace path from background image");+ auto_trace.select_action.connect((self) => {+ Glyph g = MainWindow.get_current_glyph ();+ GlyphBackgroundImage? bg = g.get_background_image ();+ GlyphBackgroundImage b;+ + if (bg != null) {+ b = (!) bg;+ g.add_path (b.auto_trace ());+ }+ });+ trace.add_tool (auto_trace);+ + draw_tools.set_open (true);+ draw_tool_modifiers.set_open (true);+ path_tool_modifiers.set_open (true);+ view_tools.set_open (true);+ grid.set_open (true);+ characterset_tools.set_open (true);+ test_tools.set_open (true);+ guideline_tools.set_open (true);+ background_tools.set_open (true);+ trace.set_open (true);+ + add_expander (draw_tools);+ add_expander (draw_tool_modifiers);+ add_expander (path_tool_modifiers); + + add_expander (characterset_tools);+ add_expander (guideline_tools);+ add_expander (grid);+ add_expander (view_tools);+ + add_expander (background_tools);+ // Fixa: add_expander (trace);+ + add_expander (test_tools);+ + draw_tools.set_persistent (true);+ draw_tools.set_unique (true);+ + draw_tool_modifiers.set_persistent (true);+ draw_tool_modifiers.set_unique (true);++ path_tool_modifiers.set_persistent (false);+ path_tool_modifiers.set_unique (false);++ characterset_tools.set_persistent (true);+ characterset_tools.set_unique (true);+ + test_tools.set_persistent (true);+ + guideline_tools.set_persistent (true);+ guideline_tools.set_unique (false);+ + grid.set_persistent (true);+ grid.set_unique (true);+ grid.set_open (false);+ + background_tools.set_persistent (true);+ background_tools.set_unique (true);+ + trace.set_persistent (false);+ trace.set_unique (false);+ + button_press_event.connect ((se, e)=> {+ foreach (var exp in expanders) {+ foreach (Tool t in exp.tool) {+ if (t.is_over (e.x, e.y)) {+ t.panel_press_action (t, e.button, e.x, e.y);+ press_tool = t;+ }+ }+ }++ return true;+ });+ + button_release_event.connect ((se, e)=> {+ release (e.button, e.x, e.y);+ return true;+ });++ motion_notify_event.connect ((sen, e)=> {+ move (e.x, e.y);+ return true;+ });+ + expose_event.connect ((t, e)=> {+ Allocation allocation;+ get_allocation (out allocation);+ + Context cw = cairo_create(get_window());+ draw (allocation, cw);+ + return true;+ });+ + add_events (EventMask.BUTTON_PRESS_MASK | EventMask.BUTTON_RELEASE_MASK | EventMask.POINTER_MOTION_MASK | EventMask.LEAVE_NOTIFY_MASK);+ + update_expanders ();+ reset_active_tool ();++ // Default selection+ var idle = new IdleSource();+ idle.set_callback (() => {+ new_point.set_selected (true);+ pen_tool.set_selected (true);+ + if (glyph_canvas.get_current_glyph ().get_show_help_lines ()) {+ help_lines.set_selected (true);+ help_lines.set_active (false);+ }++ add_new_grid ();+ + move (0, 0);+ + return false;+ });+ idle.attach (null);+ }+ + private void release (uint button, double x, double y) {+ foreach (var exp in expanders) {+ if (exp.is_over (x, y)) {+ exp.set_open (! exp.is_open ()); + if (exp is ToolboxExpander) {+ if (is_expanded ()) minimize ();+ else maximize ();+ }++ update_expanders ();+ + Allocation allocation;+ get_allocation (out allocation);+ queue_draw_area ((int) exp.x - 10, (int) exp.y - 10, allocation.width, (int) (allocation.height - exp.y + 10));+ }+ + if (exp.is_open ()) {+ foreach (Tool t in exp.tool) {+ bool active = t.is_over (x, y);+ + if (active) {+ if (press_tool == t) {+ select_tool (t);+ }+ }+ + t.panel_release_action (t, button, x, y);+ }+ }+ }+ }+ + private void select_draw_tool () {+ if (current_tool is PenTool || current_tool is CutTool) {+ return;+ }+ + select_tool_by_name ("pen_tool");+ }+ + private void move (double x, double y) {+ bool update;+ bool a;+ foreach (var exp in expanders) {+ a = exp.is_over (x, y);+ update = exp.set_active (a);+ + if (update) {+ queue_draw_area ((int) exp.x - 10, (int) exp.y - 10, (int) (exp.x + exp.w + 10), (int) (exp.y + exp.h + 10));+ }+ + if (exp.is_open ()) {+ foreach (Tool t in exp.tool) {+ bool active = t.is_over (x, y);+ TooltipArea? tpa = null;+ + update = t.set_active (active);+ tpa = MainWindow.get_tool_tip ();+ + if (active && tpa != null) {+ ((!)tpa).update_text ();+ }+ + if (update) {+ queue_draw_area (0, 0, allocation.width, allocation.height);+ }+ + t.panel_move_action (t, x, y);+ }+ }+ }+ }+ + public void remove_all_grid_buttons () {+ while (grid_expander.tool.length () > 2) {+ grid_expander.tool.remove_link (grid_expander.tool.last ());+ }+ + while (GridTool.sizes.length () > 0) {+ GridTool.sizes.remove_link (GridTool.sizes.first ());+ }+ + grid_expander.set_open (true);+ update_expanders ();+ MainWindow.get_toolbox ().queue_draw_area (0, 0, allocation.width, allocation.height); + }+ + public void parse_grid (string spin_button_value) {+ SpinButton sb = add_new_grid ();+ sb.set_value (spin_button_value);+ select_tool (sb);+ }++ public SpinButton add_new_grid () {+ SpinButton grid_width = new SpinButton ("grid_width", "Set size for grid");+ + grid_width.new_value_action.connect((self) => {+ grid_width.select_action (grid_width);+ });++ grid_width.select_action.connect((self) => {+ SpinButton sb = (SpinButton) self;+ GridTool.set_grid_width (sb.get_value ());+ MainWindow.get_glyph_canvas ().redraw ();+ });+ + grid_expander.add_tool (grid_width);++ GridTool.sizes.append (grid_width);++ grid_expander.set_open (true);+ update_expanders ();+ + MainWindow.get_toolbox ().queue_draw_area (0, 0, allocation.width, allocation.height);+ + select_tool (grid_width);+ grid_width.set_active (false);+ + return grid_width;+ }++ public void remove_current_grid () {+ Tool grid_width;+ + foreach (Tool t in grid_expander.tool) {+ if (t.is_selected () && t is SpinButton) {+ GridTool.sizes.remove ((SpinButton)t);+ grid_expander.tool.remove (t);+ break;+ }+ }+ + if (grid_expander.tool.length () > 0) {+ grid_width = grid_expander.tool.last ().data;+ select_tool (grid_width);+ grid_width.set_active (false);+ }+ + update_expanders ();+ MainWindow.get_toolbox ().queue_draw_area (0, 0, allocation.width, allocation.height);+ }++ public void reset_active_tool () {+ foreach (var exp in expanders) {+ foreach (Tool t in exp.tool) {+ t.set_active (false);+ }+ }+ }++ public Tool? get_active_tool () {+ foreach (var exp in expanders) {+ foreach (Tool t in exp.tool) {+ if (t.is_active ()) {+ return t;+ }+ }+ }+ + foreach (Tool t in MainWindow.get_content_display ().tools) {+ if (t.is_active ()) {+ return t;+ } + }+ + return null;+ }++ public Tool get_current_tool () {+ return current_tool;+ }+ + public void select_tool (Tool tool) {+ foreach (var exp in expanders) {+ + foreach (Tool t in exp.tool) {+ if (tool.get_id () == t.get_id ()) {+ exp.set_open (true);+ + bool update;+ + update = tool.set_selected (true);+ update = tool.set_active (true);+ + tool.select_action (tool); // execute command+ + if (update) { + queue_draw_area ((int) exp.x - 10, (int) exp.y - 10, allocation.width, (int) (allocation.height - exp.y + 10));+ }+ + if (exp == draw_tools || t == move_background || t == cut_background) {+ current_tool = tool;+ }+ }+ }+ + }+ }+ + public Tool get_tool (string name) {+ foreach (var e in expanders) {+ foreach (var t in e.tool) {+ if (t.get_name () == name) {+ return t;+ }+ }+ }++ if (name == cut_background.get_name ()) {+ return cut_background;+ }+ + warning ("No tool found for name \"%s\".\n", name);+ + return new Tool ("no_icon");+ }+ + public void select_tool_by_name (string name) {+ select_tool (get_tool (name));+ }+ + private void update_expanders () {+ Expander? p = null; + Expander pp;+ foreach (var e in expanders) {+ if (p != null) {+ pp = (!) p;+ e.set_offset (pp.y + pp.margin + 12);+ }+ + p = e;+ }+ }+ + private void add_expander (Expander e) {+ expanders.append (e);+ }+ + private void draw_expanders (Allocation allocation, Context cr) {+ foreach (Expander e in expanders) {+ e.draw (allocation, cr);+ if (e.is_open ()) {+ e.draw_content (allocation, cr);+ }+ }+ }+ + public void draw (Allocation allocation, Context cr) { + cr.save ();+ + cr.rectangle(0, 0, allocation.width, allocation.height);+ cr.set_line_width(0);+ cr.set_source_rgba(124/255.0, 124/255.0, 124/255.0, 1);+ cr.fill();++ cr.rectangle(0, 0, 1, allocation.height);+ cr.set_line_width(0);+ cr.set_source_rgba(0/255.0, 0/255.0, 0/255.0, 1);+ cr.fill();++ draw_expanders (allocation, cr);+ + cr.restore ();+ }+ + public bool is_expanded () {+ return expanded;+ }+ + public void maximize () {+ Allocation a;+ get_allocation (out a);+ expanded = true;+ toolbox_expander.set_open (expanded);+ set_size_request (158, a.height);+ }+ + public void minimize () {+ Allocation a;+ get_allocation (out a);+ expanded = false;+ toolbox_expander.set_open (expanded);+ set_size_request (18, a.height); + }+}++class ToolboxExpander : Expander {+ + public ToolboxExpander () {+ }+ + public override bool set_open (bool o) {+ bool r = (open != o);+ rotation = (o) ? 0 : Math.PI;+ open = o;+ return r;+ }+}++class Expander : GLib.Object {+ + public double x = 7;+ public double y = 5;++ public double w = 6;+ public double h = 5;+ + public double margin = 0;+ + protected double rotation = 0;+ protected double opacity = 0.5;+ + protected bool active = false;+ protected bool open = false;++ public List<Tool> tool;++ bool persist = false;+ bool unique = false;+ + public Expander () {+ }++ /** Returns true if tools can be used with the current canvas after+ * they have been selectes and false if they are a commands to be executed.+ */+ public bool is_persistent () {+ return persist;+ }++ /** Returns true if all other tools in thid expander should be deselected + * when a tool is selected.+ */+ public bool is_unique () {+ return unique;+ }++ public void set_persistent (bool p) {+ persist = p;+ }++ public void set_unique (bool u) {+ unique = u;+ }+ + private void update_tool_position () {+ int i = 0;+ double xt = x + 10;+ double yt = y - 4;+ foreach (var t in tool) {+ t.set_properties (xt, yt, w, h);+ + if (i == 3) {+ yt += 31;+ i = 0;+ xt = x + 10;+ } else {+ i++;+ xt += w + 3 + 25;+ }+ }+ }+ + public void set_offset (double ty) {+ y = ty;+ + if (open) {+ update_tool_position ();+ }+ }+ + public void add_tool (Tool t) {+ tool.append (t);+ update_tool_position ();+ + t.select_action.connect ((selected) => {+ if (!selected.new_selection) {+ if (is_persistent ()) {+ selected.set_selected (true);+ } else {+ selected.set_selected (false);+ }+ }+ + MainWindow.get_toolbox ().queue_draw_area ((int) x, (int) y, (int) w + 300, (int) (h + margin));+ + if (is_unique ()) {+ foreach (var deselected in tool) {+ if (selected.get_id () != deselected.get_id ()) {+ deselected.set_selected (false);+ }+ }+ }++ if (!is_persistent ()) {+ var time = new TimeoutSource(200);+ time.set_callback(() => {+ selected.set_selected (false);+ MainWindow.get_toolbox ().queue_draw_area ((int) x, (int) y, (int) w + 300, (int) (h + margin));+ return false;+ });+ time.attach(null);+ }++ selected.new_selection = false;+ });+ }+ + public bool is_over (double xp, double yp) { + return (x - 7/2.0 <= xp <= x + w + 7/2.0 && y - 7/2.0<= yp <= y + w + 7/2.0); + }+ + public bool set_active (bool a) {+ bool r = (active != a);+ opacity = (a) ? 1 : 0;+ active = a;+ return r;+ }+ + public bool is_open () {+ return open;+ }+ + public virtual bool set_open (bool o) {+ bool r = (open != o);+ rotation = (o) ? Math.PI_2 : 0;+ + if (o) {+ margin = 35 * (int)((tool.length () / 4.0) + 1) ;+ rotation = Math.PI_2; + if (tool.length () % 4 == 0) {+ margin -= 35;+ }+ } else {+ margin = 0;+ rotation = 0; + }+ + open = o;+ return r;+ }+ + public void draw (Allocation allocation, Context cr) {+ double lx, ly;+ double ih2 = 5.4 / 2;+ double iw2 = 5.4 / 2;+ + // box+ cr.save ();+ cr.set_line_join (LineJoin.ROUND);+ cr.set_line_width(7);+ cr.set_source_rgba (176/255.0, 211/255.0, 230/255.0, opacity);+ cr.rectangle (x, y, w, h);+ cr.stroke ();+ cr.restore ();+ + // arrow+ cr.save ();+ + cr.translate (x + w/2, y + h/2);+ cr.rotate (rotation);++ cr.set_line_width (1);+ cr.set_source_rgba (0, 0, 0, opacity);++ cr.new_path ();+ cr.move_to (-iw2, -ih2);+ cr.line_to (iw2, 0); + cr.line_to (-iw2, +ih2);++ cr.close_path();+ cr.stroke ();+ cr.restore ();+ + // separator+ cr.save ();+ lx = x + w + 7;+ ly = y + ih2;+ if (lx < allocation.width) {+ cr.set_line_width(1);+ cr.set_source_rgba (0, 0, 0, 0.2);+ cr.move_to (lx, ly);+ cr.line_to (allocation.width - w - x + 4, ly); + cr.stroke ();+ }+ cr.restore ();++ }+ + public void draw_content (Allocation allocation, Context cr) {+ if (open) {+ cr.save ();+ foreach (var t in tool) {+ t.draw (allocation, cr);+ }+ cr.restore ();+ }+ }+ +}++}new file mode 100644@@ -0,0 +1,120 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gdk;+using Gtk;++namespace Supplement {++class TooltipArea : DrawingArea {++ string tool_tip;++ public TooltipArea () {+ set_text_from_tool ();+ + expose_event.connect ((t, e)=> {+ Allocation alloc;+ Context cr = cairo_create (get_window ());+ + get_allocation (out alloc);+ allocation = alloc;+ + draw (cr, alloc);+ return true;+ });+ + set_size_request (10, 20);+ }++ public void update_text () {+ set_text_from_current_tool ();+ queue_draw_area (0, 0, allocation.width, allocation.height);+ }+ + public void show_text (string text) {+ tool_tip = text;+ queue_draw_area (0, 0, allocation.width, allocation.height);+ }+ + public void set_text_from_tool () {+ set_text_from_current_tool ();+ queue_draw_area (0, 0, allocation.width, allocation.height);+ }+ + private void set_text_from_current_tool () {+ Toolbox? tb = MainWindow.get_toolbox ();+ Tool? t;+ Tool tool;+ StringBuilder sb;+ + if (tb == null) {+ return;+ }+ + t = ((!)tb).get_active_tool ();+ + if (t == null) {+ return;+ }+ + tool = (!) t;+ + if (tool.key != '\0') {+ sb = new StringBuilder ();++ sb.append ("(");+ + if (tool.modifier_flag == CTRL) {+ sb.append ("Ctrl+");+ } else if (tool.modifier_flag == SHIFT) {+ sb.append ("Shift+");+ } ++ sb.append_unichar (tool.key);+ sb.append (") ");+ sb.append (tool.get_tip ());+ + show_text (sb.str);+ } else {+ show_text (tool.get_tip ());+ }+ + }+ + public void draw (Context cr, Allocation alloc) {+ cr.save ();+ cr.rectangle (0, 0, alloc.width, alloc.height);+ cr.set_line_width (0);+ cr.set_source_rgba (183/255.0, 200/255.0, 223/255.0, 1);+ cr.fill_preserve ();+ cr.stroke ();+ cr.restore ();++ cr.save ();+ cr.set_font_size (14);+ cr.select_font_face ("Cantarell", FontSlant.NORMAL, FontWeight.BOLD);+ + cr.move_to (25, 15);+ cr.show_text (tool_tip);+ cr.restore ();++ }+}++}new file mode 100644@@ -0,0 +1,52 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++namespace Supplement {++/** User interface action for unit testing and undo command.+ */+interface UserActions {+ public abstract void do_action ();+ public abstract void undo ();+ public abstract void reundo ();+ public abstract void test ();+}++interface UserClickActions {+ public abstract void do_click (int button, int x, int y);+}++class NewPointAction : UserActions, UserClickActions {++ public void do_action () {+ }++ public void undo () {+ }+ + public void reundo () {+ }+ + public void do_click (int button, int x, int y) {+ }+ + public void test () {+ }+ +}++}new file mode 100644@@ -0,0 +1,117 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Cairo;+using Gtk;+using Gdk;+using Math;++namespace Supplement {++class VersionList : DropMenu {+ public int versions = 1;+ public int current_version = 0;+ public List<Glyph> glyphs = new List<Glyph> ();++ public VersionList (Glyph? g = null) {+ base ("version");+ set_direction (MenuDirection.POP_UP);+ + MenuAction ma = add_item ("New version");+ ma.action = (self) => {+ return_if_fail (self.parent != null);+ return_if_fail (glyphs.length () > 0);+ + Supplement.get_current_font ().touch ();+ + current_version = (int) glyphs.length () - 1;+ add_new_version ();+ };+ + if (g != null) {+ add_glyph ((!) g);+ }+ }+ + public void set_current (Glyph g) {+ int i = 0;+ foreach (Glyph gl in glyphs) {+ if (likely (gl == g)) {+ current_version = i;+ return;+ }+ i++;+ }+ + warning ("Glyph not found in list.");+ }+ + public Glyph get_current () {+ if (unlikely (current_version >= glyphs.length ())) {+ warning (@"current_version >= glyphs.length ($current_version)");+ return new Glyph ("");+ }+ + return glyphs.nth (current_version).data;+ }++ public void add_new_version () {+ Glyph g = get_current ();+ Glyph new_version = g.copy ();+ TabBar tb = MainWindow.get_tab_bar ();+ + tb.close_background_tab_by_name (g.name);+ add_glyph (new_version);+ }+ + private void set_selected_item (MenuAction ma) {+ Glyph g = get_current ();+ int i = ma.index;+ + return_if_fail (0 <= i < glyphs.length ());+ + MainWindow.get_tab_bar ().close_by_name (g.name);+ current_version = i;+ + return_if_fail (ma.parent != null);+ + ((!)ma.parent).deselect_all ();+ ma.set_selected (true); + }+ + public void add_glyph (Glyph new_version, bool selected = true) {+ MenuAction ma;+ + versions++;+ glyphs.append (new_version);++ ma = add_item (@"Version $(versions - 1)");+ ma.index = (int) glyphs.length () - 1;+ ma.action = (self) => {+ Font font = Supplement.get_current_font ();+ set_selected_item (self);+ font.touch ();+ };+ + if (selected) {+ set_selected_item (ma);+ }+ }++}++}new file mode 100644@@ -0,0 +1,157 @@+/*+ Copyright (C) 2012 Johan Mattsson++ This program is free software: you can redistribute it and/or modify+ it under the terms of the GNU 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 General Public License for more details.++ You should have received a copy of the GNU General Public License+ along with this program. If not, see <http://www.gnu.org/licenses/>.+*/++using Gtk;++namespace Supplement {++class ZoomTool : Tool {++ int zoom_area_begin_x = -1;+ int zoom_area_begin_y = -1;+ + int view_index = 0;+ List<Tab> views = new List<Tab> ();+ + public ZoomTool (string n) {+ base (n, "Zoom", 'z');++ select_action.connect((self) => {+ });++ select_action.connect((self) => {+ });+ + press_action.connect((self, b, x, y) => {+ if (b == 1) {+ zoom_area_begin_x = x;+ zoom_area_begin_y = y;+ }+ });++ move_action.connect((self, x, y) => {+ if (zoom_area_begin_x > 0) {+ Glyph g = MainWindow.get_current_glyph ();+ g.show_zoom_area (zoom_area_begin_x, zoom_area_begin_y, x, y);+ }+ });+ + release_action.connect((self, b, x, y) => {+ Glyph g;+ + if (b == 1) {+ store_current_view ();+ + g = MainWindow.get_current_glyph ();+ + if (zoom_area_begin_x == x && zoom_area_begin_y == y) { // zero width center this point but don't zoom in+ g.set_center (x, y);+ } else { + g.set_zoom_from_area ();+ }+ + zoom_area_begin_x = -1;+ zoom_area_begin_y = -1;+ }+ });+ }++ public void zoom_full_background_image () {+ GlyphBackgroundImage bg;+ Glyph g = MainWindow.get_current_glyph ();+ int x, y;+ + if (g.get_background_image () == null) {+ return;+ }+ + bg = (!) g.get_background_image ();+ + x = (int) (Glyph.xc () + bg.img_offset_x);+ y = (int) (Glyph.yc () + bg.img_offset_y);+ + g.set_zoom_area (x, y, (int) (x + bg.get_current_width ()), (int) (y + bg.get_current_height ()));+ g.set_zoom_from_area ();+ }++ public void zoom_full_glyph () {+ store_current_view ();+ + MainWindow.get_current_display ().zoom_min ();+ }+ + public override bool test () {+ test_select_action ();+ return true;+ }+ + /** Add an item to zoom view list. */+ public void store_current_view () { + if (views.length () - 1 > view_index) {+ unowned List<Tab> i = views.nth (view_index + 1);+ while (i != i.last ()) {+ i.delete_link (i.next);+ }+ }+ + views.append (MainWindow.get_current_tab ());+ view_index = (int)views.length () - 1;+ MainWindow.get_current_display ().store_current_view ();+ }++ /** Redo last zoom.*/+ public void next_view () {+ if (view_index + 1 >= (int) views.length ()) {+ return;+ }+ + view_index++;+ + MainWindow.select_tab (views.nth (view_index).data);+ MainWindow.get_current_display ().next_view ();+ MainWindow.get_glyph_canvas ().redraw ();+ }+ + /** Undo last zoom. */+ public void previous_view () {+ if (view_index == 0) {+ return;+ }+ + view_index--;+ + MainWindow.select_tab (views.nth (view_index).data);+ MainWindow.get_current_display ().restore_last_view ();+ MainWindow.get_glyph_canvas ().redraw ();+ }+}++class ZoomView : GLib.Object {+ public double x;+ public double y;+ public double zoom;+ public Allocation allocation;+ + public ZoomView (double x, double y, double zoom, Allocation allocation) {+ this.x = x;+ this.y = y;+ this.zoom = zoom;+ this.allocation = allocation;+ }+}+ +}new file mode 100644@@ -0,0 +1,64 @@+SUPPLEMENT_MAIN = [ + 'Font.vala', + 'BackgroundTool.vala', + 'BackgroundSelection.vala', + 'ExportTool.vala', + 'FontDisplay.vala', + 'CharDatabase.vala', + 'Context.vala', + 'ContentDisplay.vala', + 'CutBackgroundTool.vala', + 'CutTool.vala', + 'ClipTool.vala', + 'DropMenu.vala', + 'EditPoint.vala', + 'EditPointHandle.vala', + 'FileSelection.vala', + 'Glyph.vala', + 'GlyphBackgroundImage.vala', + 'GlyphCanvas.vala', + 'GlyphCollection.vala', + 'GlyphRange.vala', + 'GridTool.vala', + 'Icons.vala', + 'Kerning.vala', + 'KeyBindings.vala', + 'Line.vala', + 'MainWindow.vala', + 'MergeTool.vala', + 'MoveTool.vala', + 'IOThread.vala', + 'OpenFontFormatWriter.vala', + 'OverView.vala', + 'Path.vala', + 'PenTool.vala', + 'ResizeTool.vala', + 'Supplement.vala', + 'SaveDialog.vala', + 'ShrinkTool.vala', + 'SpinButton.vala', + 'Svg.vala', + 'SvgFontFormatWriter.vala', + 'Preferences.vala', + 'TabBar.vala', + 'TestSupplement.vala', + 'Tool.vala', + 'Toolbox.vala', + 'TooltipArea.vala', + 'VersionList.vala', + 'ZoomTool.vala' +] + +TEST = [ + 'TestSupplement.vala' +] + +def build (bld): + bld.program ( + target = 'supplement', + param = '--enable-experimental --enable-experimental-non-null --vapidir=../ ', + packages = 'gtk+-2.0 libxml-2.0', + uselib = 'GTK GLIB GIO XML', + source = SUPPLEMENT_MAIN + ) + new file mode 100755Binary files /dev/null and b/waf differnew file mode 100644@@ -0,0 +1,22 @@+VERSION = '0.1' +APPNAME = 'Supplement' + +top = '.' +out = 'build' + +def options(opt): + opt.load('compiler_c') + opt.load('vala') + +def configure(conf): + conf.load('compiler_c vala') + + conf.check_cfg(package='glib-2.0', uselib_store='GLIB',atleast_version='2.16.0', mandatory=1, args='--cflags --libs') + conf.check_cfg(package='gio-2.0', uselib_store='GIO', atleast_version='2.16.0', mandatory=1, args='--cflags --libs') + conf.check_cfg(package='gtk+-2.0', uselib_store='GTK', atleast_version='2.16.0', mandatory=1, args='--cflags --libs') + conf.check_cfg(package='libxml-2.0', uselib_store='XML', mandatory=1, args='--cflags --libs') + + conf.env.append_unique('VALAFLAGS', ['--enable-experimental', '--enable-experimental-non-null', '--vapidir=../']) + +def build(bld): + bld.recurse('src')